Compare commits

..

247 Commits

Author SHA1 Message Date
Jeff Avallone
54b48f5581 Improving test coverage 2018-06-08 21:05:35 -04:00
Jeff Avallone
52d378be61 Removing jest-junit
Not using CircleCI anymore, so don't need it
2018-06-08 15:10:51 -04:00
Jeff Avallone
f7c803a3a6 Removing need for getDerivedStateFromProps 2018-06-07 22:13:15 -04:00
Jeff Avallone
d3a0746ebb Removing unnecessary constructor 2018-06-07 21:53:15 -04:00
Jeff Avallone
03ef9b2203 Removing badges from README 2018-06-06 16:41:07 -04:00
Jeff Avallone
6c934bcb0f Fixing invalidation ID for deployments 2018-06-06 01:33:31 -04:00
Jeff Avallone
10196afec5 Updating CI config to build correctly 2018-06-05 16:37:43 -04:00
Jeff Avallone
79fa7d073d Setting image to use for builds 2018-06-05 08:41:15 -04:00
Jeff Avallone
04cec072e3 Tagging pipeline steps to use shared runners 2018-06-05 07:18:41 -04:00
Jeff Avallone
a0468fd06f Adding GitLab CI config 2018-06-05 07:09:06 -04:00
Jeff Avallone
99b28eb5f2 Adding GitLab CI status badges 2018-06-05 06:40:21 -04:00
Jeff Avallone
472273e7e2 Migrating to GitLab (CI config TBD) 2018-06-04 21:23:27 -04:00
Jeff Avallone
76785dcfa8 Package upgrades 2018-06-02 13:35:58 -04:00
Jeff Avallone
14eeab2098 Upgrading Jest 2018-06-02 13:13:37 -04:00
Jeff Avallone
fd656c8649 Upgrading React and Form's getDerivedStateFromProps 2018-06-02 13:01:16 -04:00
Jeff Avallone
7a8e90d3ca Style tweaks 2018-06-02 06:43:03 -04:00
Jeff Avallone
919612bc0d Fixing translation behavior of download link labels 2018-06-01 17:59:24 -04:00
Jeff Avallone
cda91cd3f5 Adding "English (ALL-CAPS)" as a language
This is to provide a sample language other than English. Since I don't
speak any other languages with anything even approaching enough fluency
to do a translation, this joke of a "translation" will have to do.
2018-06-01 17:50:55 -04:00
Jeff Avallone
a3de3e522f I should check the file extension used before commiting the docs 2018-06-01 17:41:54 -04:00
Jeff Avallone
60e524c134 Fixing wording in the Privacy Policy 2018-06-01 17:37:45 -04:00
Jeff Avallone
295739b079 Adding docs for updating translatoins 2018-06-01 17:36:32 -04:00
Jeff Avallone
6b8a92df8b Making language switcher a styled select list 2018-06-01 17:19:52 -04:00
Jeff Avallone
12436fbd9e Extracting select list styles to be re-used 2018-06-01 17:19:27 -04:00
Jeff Avallone
9ff0a51006 Moving Privacy Policy link to the header 2018-06-01 17:18:23 -04:00
Jeff Avallone
81aceeba5f Modifying separated inline list styles to wrap neatly
The "//" separator is hidden when the list items wrap.
2018-06-01 17:16:54 -04:00
Jeff Avallone
8eb2398de1 Adjusting browser list and cssnext configs 2018-06-01 17:12:35 -04:00
Jeff Avallone
befa23b4ea Adding missing translation 2018-06-01 17:11:48 -04:00
Jeff Avallone
2656edf27e Adding LocaleSwitcher to Header 2018-05-31 19:07:29 -04:00
Jeff Avallone
2fc961725a Updating Header styles to allow right-hand items to reflow 2018-05-31 19:07:11 -04:00
Jeff Avallone
efc8f6744a Adding a LocaleSwitch component
Still needs some styling, but it is functional
2018-05-31 19:06:33 -04:00
Jeff Avallone
479d035cd0 Updating i18n:scrub script to remove defined keys from missing namespace 2018-05-31 18:23:04 -04:00
Jeff Avallone
b575b9a0cc Adding check to deploy script for file to delete before attempting 2018-05-31 17:26:32 -04:00
Jeff Avallone
1eb3ca9182 Fixing typo 2018-05-31 17:17:23 -04:00
Jeff Avallone
a2b9808613 Adding PWA install prompt 2018-05-29 19:01:19 -04:00
Jeff Avallone
589b1aa9e0 Tweaking test summary setup 2018-05-29 17:36:29 -04:00
Jeff Avallone
49e56aebd7 Setting up CircleCI test summary support 2018-05-29 17:30:51 -04:00
Jeff Avallone
48903eb54e Using the AWS SDK correctly 2018-05-29 17:13:38 -04:00
Jeff Avallone
43160ca71e Fixing un-updated config value name 2018-05-29 17:11:20 -04:00
Jeff Avallone
16e900e5df Invalidating CloudFront cache when deploying 2018-05-29 17:08:22 -04:00
Jeff Avallone
41676a370f Removing sample messages from App 2018-05-28 16:08:05 -04:00
Jeff Avallone
e71f1b4cc2 Cleaning up console.log calls in tests 2018-05-28 14:48:10 -04:00
Jeff Avallone
8ba954c743 Loading rendering code dynamically 2018-05-28 14:41:02 -04:00
Jeff Avallone
d88defcc65 Moving logic to generate SVG and PNG blobs to SVG/Image
This is to avoid reaching into the Image component to access the SVG ref
2018-05-28 12:47:49 -04:00
Jeff Avallone
3916d63f2d Switching to use forwardRef 2018-05-28 12:28:08 -04:00
Jeff Avallone
b9f6766a66 Using React.createRef 2018-05-28 12:16:44 -04:00
Jeff Avallone
41f10c0dc3 Removing syntax map from devel.js 2018-05-28 12:10:15 -04:00
Jeff Avallone
9a10fc35e8 Adjusting chunk splitting config 2018-05-28 12:03:49 -04:00
Jeff Avallone
2a4e195742 Removing unnecessary config 2018-05-28 11:21:25 -04:00
Jeff Avallone
6ced6fddd5 Renaming CircleCI contexts to be more specific 2018-05-28 11:12:52 -04:00
Jeff Avallone
ab786ae242 Updating build status badge 2018-05-28 11:02:35 -04:00
Jeff Avallone
a22e858648 Adding documentation about configuration via environment variables 2018-05-28 11:02:13 -04:00
Jeff Avallone
7aa95e092b Updatnig README scripts section 2018-05-28 10:38:35 -04:00
Jeff Avallone
537bf3eb11 Renaming deploy config file 2018-05-28 10:33:51 -04:00
Jeff Avallone
c7a7acc1c9 Fixing incorrect argument 2018-05-27 13:49:55 -04:00
Jeff Avallone
25197b9abd Adding more flexible deploy script 2018-05-27 13:45:26 -04:00
Jeff Avallone
4685337599 Adjusting styles 2018-05-25 15:21:31 -04:00
Jeff Avallone
ced39a279f Typo 2018-05-25 14:54:11 -04:00
Jeff Avallone
f2d10483ad Fixing content-type for service-worker in S3 2018-05-25 14:49:07 -04:00
Jeff Avallone
92800241d7 Setting cache-control header for service-worker.js file 2018-05-25 14:37:25 -04:00
Jeff Avallone
beb73f3e69 Adjusting service worker config 2018-05-25 14:37:07 -04:00
Jeff Avallone
8f50e2e232 Turning off cache for local prod server 2018-05-25 14:36:33 -04:00
Jeff Avallone
cb0c39b081 Whitespace 2018-05-25 14:36:16 -04:00
Jeff Avallone
133e0c9ec3 Adding build ID to Footer component 2018-05-25 14:35:32 -04:00
Jeff Avallone
d63d2484da Adding some logging when Do Not Track disables stuff 2018-05-24 18:04:57 -04:00
Jeff Avallone
99ecc123f4 Refering to the privacy policy from JS Disabled error message 2018-05-24 17:57:20 -04:00
Jeff Avallone
54a45ea3be Linking privacy policy in the footer 2018-05-24 17:56:35 -04:00
Jeff Avallone
821c2eb7fe Adding privacy policy page 2018-05-24 17:56:35 -04:00
Jeff Avallone
031dc2235a Adding support for "info"-type Messages 2018-05-24 17:56:35 -04:00
Jeff Avallone
1a8bb762cb Supporting "Do Not Track" for Gogle Analytics and Sentry error reporting 2018-05-24 17:01:17 -04:00
Jeff Avallone
7698901451 Fixing service worker URL 2018-05-16 18:47:02 -04:00
Jeff Avallone
f3a2acdd7f Updating dependencies 2018-05-16 18:46:58 -04:00
Jeff Avallone
0ac27391c7 Updating packages 2018-04-08 22:07:52 -04:00
Jeff Avallone
f0062c94be Switching to use React.createRef 2018-03-31 11:44:26 -04:00
Jeff Avallone
f5f30a854b Ensuring react-test-renderer is the correct version and enabling test 2018-03-31 05:18:54 -04:00
Jeff Avallone
1b87abb4fe Updating chunk splitting config 2018-03-30 17:06:33 -04:00
Jeff Avallone
0b76e5979c Adding webpack bundle analyzer 2018-03-30 17:06:11 -04:00
Jeff Avallone
a471a33014 Splitting prod configs 2018-03-30 17:05:33 -04:00
Jeff Avallone
5c2b06d4e9 Upgrading i18next packages 2018-03-29 21:34:25 -04:00
Jeff Avallone
2d19ae09bf Using a path in favicon SVG since font wasn't loading 2018-03-29 21:31:48 -04:00
Jeff Avallone
8ef5a5483d Upgrading Workbox plugin 2018-03-29 21:25:45 -04:00
Jeff Avallone
a3ad7d0e08 Upgrading webpack packages 2018-03-29 21:10:21 -04:00
Jeff Avallone
50a2339cc1 Updating more packages 2018-03-29 20:16:21 -04:00
Jeff Avallone
cd6ccf5838 Upgrading eslint packages 2018-03-29 20:10:38 -04:00
Jeff Avallone
0234371309 Upgrading Jest and related dependencies 2018-03-29 19:05:23 -04:00
Jeff Avallone
fe7caf5faa Updating React and switching to new lifecycle methods 2018-03-29 19:00:52 -04:00
Jeff Avallone
91b6ac8668 Upgrading other packages 2018-03-07 19:09:27 -05:00
Jeff Avallone
cbe538c007 Upgrading webpack to 4.0
Needed to replace favicon generator
2018-03-07 19:09:08 -05:00
Jeff Avallone
e28fd69c73 Tweaking cache regexes 2018-02-25 17:33:55 -05:00
Jeff Avallone
9c9d3141e9 Removing unnecessary code 2018-02-25 11:49:29 -05:00
Jeff Avallone
18bd368525 Removing return from reflow implementations
I don't think it will be necessary to wait for components to complete
re-rendering before moving on with the reflow process. Parent components
only depend on the bounding box and that is determined immediately when
reflow is called
2018-02-25 11:47:57 -05:00
Jeff Avallone
1e8ee71aef Updating dependencies 2018-02-24 17:07:22 -05:00
Jeff Avallone
e5e6f1d0c8 Adding font and lone external image to the service worker cache 2018-02-24 16:43:08 -05:00
Jeff Avallone
692b9fa2df Cleaning up tests 2018-02-20 21:00:48 -05:00
Jeff Avallone
f90b4a7bed Adding tests for updating the expression and syntax 2018-02-20 21:00:48 -05:00
Jeff Avallone
33a473734b Updating tests to cover an edge case 2018-02-20 20:25:06 -05:00
Jeff Avallone
aa278fb193 Adding URL handling 2018-02-19 17:29:08 -05:00
Jeff Avallone
50d05c423d Using modules in babel
Switching back to running tests in production environment
2018-02-19 17:01:52 -05:00
Jeff Avallone
2dd2132a2b Revert "Running tests in production environment to remove spurious warnings"
This reverts commit 0a9b0f6bfb.
2018-02-18 16:26:36 -05:00
Jeff Avallone
0a9b0f6bfb Running tests in production environment to remove spurious warnings
This gets rid of the warnings from React due to jsdom not recognizing
SVG elements. Looks like there's a PR against jsdom that might fix this,
so once that's in this change can be reverted
2018-02-18 16:11:25 -05:00
Jeff Avallone
ef33cdab04 Adding tests for Path#arcTo 2018-02-18 16:03:46 -05:00
Jeff Avallone
d6cdad7ec3 Adding tests for Path#quadraticCurveTo 2018-02-18 16:01:24 -05:00
Jeff Avallone
354b65b623 Adding tests for cubicCurveTo 2018-02-18 15:58:23 -05:00
Jeff Avallone
9716e166df Adding test for Path#closePath 2018-02-18 15:52:40 -05:00
Jeff Avallone
45b652d9af Adding tests for Path#moveTo 2018-02-18 15:49:20 -05:00
Jeff Avallone
bb37848265 Adding specs and some refactoring for the Path#lineTo method 2018-02-18 15:49:03 -05:00
Jeff Avallone
381df8bf93 Adding tests for root SVG component 2018-02-18 11:49:09 -05:00
Jeff Avallone
6285bd4320 Upgrading packages 2018-02-17 22:26:24 -05:00
Jeff Avallone
cf824ac334 Adjusting text color to improve contrast 2018-02-17 22:19:39 -05:00
Jeff Avallone
a1ef89b6fb Refining header styles 2018-02-17 22:07:49 -05:00
Jeff Avallone
9eea045d2b Fixing centering of rendered SVG when container is too small 2018-02-17 17:25:54 -05:00
Jeff Avallone
1362ceb8c8 Adding slight text-shadow to header to get logo popping a little 2018-02-17 17:21:18 -05:00
Jeff Avallone
6e8d7c297a Moving to using a decorator mixin instead of base class for SVGs 2018-02-17 16:58:49 -05:00
Jeff Avallone
44e6dae289 Simplifying "pre-reflow" logic 2018-02-17 16:28:46 -05:00
Jeff Avallone
1f9ba28099 Removing an unused property 2018-02-17 16:27:18 -05:00
Jeff Avallone
6c4972e726 Updating how initial state is set 2018-02-17 16:14:59 -05:00
Jeff Avallone
13dc496a02 Some optimization of immutable object use 2018-02-17 16:13:19 -05:00
Jeff Avallone
c7aca59afc Tweaking ExtractTextPlugin config 2018-02-17 15:36:15 -05:00
Jeff Avallone
d2651c585f Turn off modules in babel build (webpack handles it) 2018-02-17 15:20:23 -05:00
Jeff Avallone
6ff9145603 Adding tests for SVG components
Jest/enzyme/jsdom is kicking out some nastly looking messages because it
doesn't recognize various SVG elements, but they appear to be harmless.
2018-02-17 13:04:19 -05:00
Jeff Avallone
19d34a4d9e Adding extraneous PropType 2018-02-17 12:50:27 -05:00
Jeff Avallone
f364673388 Adding promisified setState and simplfying reflow code for SVG stuff 2018-02-17 12:06:35 -05:00
Jeff Avallone
e04e4edc1f Renaming type property to theme 2018-02-17 11:23:20 -05:00
Jeff Avallone
3ead0c13df Refactoring rendering root to be a component 2018-02-17 11:16:05 -05:00
Jeff Avallone
dea6d92272 Reorganizing the rendering flow 2018-02-17 10:45:03 -05:00
Jeff Avallone
2a0e0149fd Toning down borders on literal and escape boxes 2018-02-17 10:26:01 -05:00
Jeff Avallone
c047dab5a4 Adding style for anchor elements 2018-02-17 06:24:50 -05:00
Jeff Avallone
06a90429ff Getting Download links working 2018-02-16 22:47:53 -05:00
Jeff Avallone
6bc4306ca3 Fleshing out expample render and cleaning up some SVG code 2018-02-16 21:48:01 -05:00
Jeff Avallone
82b780e9c3 First cut of SVG rendering components
These still need work, but they're functional enough to render a diagram
2018-02-16 19:16:30 -05:00
Jeff Avallone
3fdc74bdf2 Adding color output to prerender script and relocating to script 2018-02-16 17:16:23 -05:00
Jeff Avallone
d69ab00ad5 Updating i18n:scrub command with better output 2018-02-16 17:15:45 -05:00
Jeff Avallone
8e8fbd3219 Making GitHub and CC license links open in new window 2018-02-16 17:14:29 -05:00
Jeff Avallone
0d41100e04 Updating formatting 2018-02-15 21:21:43 -05:00
Jeff Avallone
5218906385 Adding script to scrub locale files
For any keys in en/translation.yaml and missing in
<lang>/translation.yaml, this tool will add the key to
<lang>/missing.yaml. This is to facilitate translation of keys that have
been added to the app.

This also serves as a lint-like tool for translation files since they
will all be re-written when running.
2018-02-15 21:19:25 -05:00
Jeff Avallone
adba2999bf Updating prerender script to use async/await 2018-02-15 20:26:03 -05:00
Jeff Avallone
17e8be5f42 Updating packages 2018-02-15 20:03:10 -05:00
Jeff Avallone
9adaa6041d Formatting 2018-02-15 20:03:04 -05:00
Jeff Avallone
8a18304225 Tweaking design of error and warning messages 2018-02-15 19:46:11 -05:00
Jeff Avallone
521f7965b0 Making border in select less pronounced 2018-02-15 19:32:25 -05:00
Jeff Avallone
74f1513311 Moving configs into package.json 2018-02-15 18:17:40 -05:00
Jeff Avallone
d634985698 Setting environment reported to Sentry separate from NODE_ENV
For preview and prod, NODE_ENV is "production"
2018-02-15 17:55:37 -05:00
Jeff Avallone
f3e3e7922c Adding line next to select arrow 2018-02-15 17:52:04 -05:00
Jeff Avallone
569c06b041 Softening header shadow and adding shadows to messages 2018-02-15 17:48:30 -05:00
Jeff Avallone
abe646cec8 Adding tests for page components 2018-02-15 17:35:59 -05:00
Jeff Avallone
8b2ce32b75 Fleshing out Form tests 2018-02-15 17:35:45 -05:00
Jeff Avallone
7a8a9836aa Adding to i18n mocks 2018-02-15 17:35:12 -05:00
Jeff Avallone
79191c0fd7 Improving coverage in RavenError tests 2018-02-15 17:33:43 -05:00
Jeff Avallone
d9af19ca63 Cleanup setup modules 2018-02-15 17:33:14 -05:00
Jeff Avallone
fcba4b75ec Moving Jest setup and consolidating 2018-02-15 17:27:15 -05:00
Jeff Avallone
2904519ff5 Tweaking for styles 2018-02-15 17:24:55 -05:00
Jeff Avallone
927718832b Cleaning up back copy-paste job 2018-02-15 17:23:15 -05:00
Jeff Avallone
eb384831fe Tweaking Jest config 2018-02-15 17:22:25 -05:00
Jeff Avallone
895827e881 Moving list styles complete to use @apply 2018-02-15 09:08:50 -05:00
Jeff Avallone
672ded87af Fixing positoining of action links in Form 2018-02-15 09:05:07 -05:00
Jeff Avallone
bec4279c31 Cleanup tests 2018-02-13 21:38:10 -05:00
Jeff Avallone
6cf064eaf0 Fleshing out messaging 2018-02-13 21:36:10 -05:00
Jeff Avallone
d328727ceb Adding support for default icons based on Message type 2018-02-13 21:28:31 -05:00
Jeff Avallone
b83f5cd34d Adding Form component 2018-02-13 21:23:49 -05:00
Jeff Avallone
597cce4566 Extracting green gradient 2018-02-13 21:23:33 -05:00
Jeff Avallone
364139d362 Extracting translation mock 2018-02-13 21:23:11 -05:00
Jeff Avallone
3931cee8af Updating import paths in tests 2018-02-13 20:18:26 -05:00
Jeff Avallone
91ee254477 Moving App into a directory and adding tests 2018-02-13 20:13:11 -05:00
Jeff Avallone
8817b5f027 Setting up webpack resolve root 2018-02-13 20:09:52 -05:00
Jeff Avallone
6b55f1ec72 CSS modules EVERYWHERE! 2018-02-13 18:15:18 -05:00
Jeff Avallone
6ab4978a03 Documenting scripts in README 2018-02-13 17:57:03 -05:00
Jeff Avallone
d8a8177f41 Setting NODE_ENV for builds 2018-02-13 17:54:14 -05:00
Jeff Avallone
888336cbec Adding loader for i18next translation files 2018-02-13 17:46:44 -05:00
Jeff Avallone
5ab4a70414 Adjusting formatting on 404 page title and adding PropTypes 2018-02-13 17:34:22 -05:00
Jeff Avallone
94e511bb96 Further improvements to page prerendering 2018-02-13 17:28:47 -05:00
Jeff Avallone
44a9cad9b3 Removing unused PageTemplate component 2018-02-13 17:26:32 -05:00
Jeff Avallone
24062d978a Improving SVG mocking for tests 2018-02-13 17:17:55 -05:00
Jeff Avallone
1fd797f52a Adding identity-obj-proxy for CSS module mocks 2018-02-13 17:15:33 -05:00
Jeff Avallone
20adf55c11 Breaking up main stylesheet into per-component styles 2018-02-13 17:12:02 -05:00
Jeff Avallone
7238643740 Reworking static page generation
Including styles in components wasn't working with the old system.
2018-02-13 17:10:32 -05:00
Jeff Avallone
ad6583d5dc Reorganizing components into directories 2018-02-12 20:05:47 -05:00
Jeff Avallone
21146549f7 Adding language detection 2018-02-11 18:43:11 -05:00
Jeff Avallone
355ef79d20 Adding i18next 2018-02-11 18:37:07 -05:00
Jeff Avallone
63766e84e9 Breaking Header and Footer out into components
For forthcoming i18n setup
2018-02-11 15:58:59 -05:00
Jeff Avallone
4923bbd985 Adding some styling to messages to spice up errors 2018-02-11 15:27:01 -05:00
Jeff Avallone
63e56c5df7 Setting browserslist 2018-02-11 14:56:39 -05:00
Jeff Avallone
eda8daf8dd Switching to use cssnext 2018-02-11 14:51:17 -05:00
Jeff Avallone
d4d764f81e Storing build and coverage report 2018-02-11 13:12:23 -05:00
Jeff Avallone
ca41d2015a Adding test coverage 2018-02-11 13:10:24 -05:00
Jeff Avallone
152bf1e361 Updating noscript statement 2018-02-11 12:49:02 -05:00
Jeff Avallone
c81943628c Fixing production sourcemaps 2018-02-11 12:37:41 -05:00
Jeff Avallone
4600d1748a Doing the short hash correctly 2018-02-11 12:18:29 -05:00
Jeff Avallone
e0716ef683 Adding tests for RavenBoundary 2018-02-11 12:13:15 -05:00
Jeff Avallone
edf4ecd081 Removing react-test-renderer 2018-02-11 11:58:03 -05:00
Jeff Avallone
7caf439c53 Adding tests for RavenError 2018-02-11 11:57:23 -05:00
Jeff Avallone
5f11a11ba2 Updating PageTemplate tests to use enzyme 2018-02-11 11:52:35 -05:00
Jeff Avallone
637c9c2afd Updating Message tests to use enzyme 2018-02-11 11:50:08 -05:00
Jeff Avallone
cabc2adc1f Adding enzyme 2018-02-11 11:49:50 -05:00
Jeff Avallone
90e57e26e7 Updating PageTemplate tests 2018-02-11 11:25:15 -05:00
Jeff Avallone
6ec546ace1 Adding error boundary using Sentry.io 2018-02-11 11:18:27 -05:00
Jeff Avallone
8b86ddc14c Updating babel and eslint configs
Adding support for class properties amongst other things
2018-02-11 11:16:07 -05:00
Jeff Avallone
5afca2241a Fixing some propTypes 2018-02-11 10:57:41 -05:00
Jeff Avallone
b009d078b6 Adding Sentry.io 2018-02-11 10:41:03 -05:00
Jeff Avallone
6bf094a4c1 Declaritively initializing service worker 2018-02-11 10:29:35 -05:00
Jeff Avallone
fd5a8786d0 Adding Google Analytics 2018-02-11 10:27:43 -05:00
Jeff Avallone
214a9eb5c1 I hate typos 2018-02-11 10:08:23 -05:00
Jeff Avallone
1e07be44da Trying install with more output 2018-02-11 10:06:59 -05:00
Jeff Avallone
1e45520e67 Updating packages before attempting install 2018-02-11 09:59:54 -05:00
Jeff Avallone
6dcd0595ae More fixes to deploy scripting 2018-02-11 09:58:44 -05:00
Jeff Avallone
9abbb86f2c Fixing awscli installation for deploy 2018-02-11 09:57:10 -05:00
Jeff Avallone
a30d1f07fc Mocking environment for PageTemplate tests 2018-02-11 09:45:46 -05:00
Jeff Avallone
70455ea2b8 Moving build-time deps out of devDependencies 2018-02-11 09:41:03 -05:00
Jeff Avallone
37af8c24c8 Adding deploy scripting 2018-02-11 09:22:37 -05:00
Jeff Avallone
bbdacca1da Adding an environment banner to the page
To prevent testing local changes in production
2018-02-11 09:16:33 -05:00
Jeff Avallone
84df219d7f Creating separate templates for each page 2018-02-11 07:42:52 -05:00
Jeff Avallone
95a6709ec0 Pulling most of the page template into a component 2018-02-11 07:28:34 -05:00
Jeff Avallone
bdf54945fe Keeping description consistent across app 2018-02-11 07:11:16 -05:00
Jeff Avallone
eef5d50436 Tweaking some spacing 2018-02-11 07:09:03 -05:00
Jeff Avallone
69999fa948 Fixing postcss config 2018-02-11 07:04:36 -05:00
Jeff Avallone
49a236bf89 Switching back to SVG components for icons
No longer need the hack to only use them in components while URLs are
used in styles since I don't need icons in the styles anymore. Embedding
the markup also provides the option to restyle the icons
2018-02-11 06:58:59 -05:00
Jeff Avallone
1de5079aa9 Rendering page template using React
Allows for using components (like Message) when rendering the base page
template
2018-02-11 06:53:34 -05:00
Jeff Avallone
810b37aa47 Removing SVG components
Sticking with importing data URIs for now
2018-02-11 06:51:54 -05:00
Jeff Avallone
c368e9031f Reworking Message component
* Supporting URLs for icon
* Moving styles into top-level stylesheet for use in the template
2018-02-11 06:41:15 -05:00
Jeff Avallone
b3e4bd2cff Switching to using latest node image 2018-02-10 21:01:58 -05:00
Jeff Avallone
4e46aa76be Removing debug output 2018-02-10 20:58:59 -05:00
Jeff Avallone
adb579ed8b Fixing job names 2018-02-10 20:57:52 -05:00
Jeff Avallone
189dfe29f2 Tweaking CI config
Trying out Contexts
2018-02-10 20:57:13 -05:00
Jeff Avallone
a3d6717786 Adding start of preview/prod deployments in CircleCI config 2018-02-10 20:49:11 -05:00
Jeff Avallone
026cc1fcea Limiting CircleCI build to only react branch 2018-02-10 20:30:33 -05:00
Jeff Avallone
9f5bb8faf1 Trying out CircleCI 2018-02-10 20:25:00 -05:00
Jeff Avallone
c38a9ddb3b Reworking test commands to include lint with unit tests
Also adding a test:watch command
2018-02-10 19:23:56 -05:00
Jeff Avallone
10125066d9 Fixing URLs in yarn.lock
Also adding a project yarnrc so I don't end up using my internal npm
repo again.
2018-02-10 19:15:35 -05:00
Jeff Avallone
5050291ab7 Initial tweaking to TravisCI script 2018-02-10 18:34:50 -05:00
Jeff Avallone
ef4259908c Using gitignore for eslint 2018-02-10 18:31:16 -05:00
Jeff Avallone
3e729b2a34 Adding Jest 2018-02-10 18:30:07 -05:00
Jeff Avallone
916b38c6c5 Moving webpack-dev-server to dev dependencies 2018-02-10 18:14:22 -05:00
Jeff Avallone
738902d0ce Adding precommit hook 2018-02-10 18:11:30 -05:00
Jeff Avallone
cf0c175d0a Addressing lint errors 2018-02-10 18:10:30 -05:00
Jeff Avallone
cca35117f5 Adding eslint 2018-02-10 18:03:21 -05:00
Jeff Avallone
74d622c7a4 Getting icon loading in the root styles working 2018-02-10 17:55:48 -05:00
Jeff Avallone
2377cb2497 Removing unnecessary imports 2018-02-10 17:27:30 -05:00
Jeff Avallone
1576904f9c Ahem... 2018-02-10 17:20:56 -05:00
Jeff Avallone
5984f59063 Adding Message component 2018-02-10 17:20:04 -05:00
Jeff Avallone
7ef40cba9c Adding app frame 2018-02-10 17:01:30 -05:00
Jeff Avallone
f8aaeba7a9 Going to one template for all pages 2018-02-10 16:39:38 -05:00
Jeff Avallone
8eb4b450ca Adding SVG icons 2018-02-10 16:32:52 -05:00
Jeff Avallone
e52103a516 Integrating React 2018-02-10 16:22:59 -05:00
Jeff Avallone
5e8501b25e Adding humans and robots files 2018-02-10 16:12:11 -05:00
Jeff Avallone
40d08ddef8 Adding service worker cache 2018-02-10 16:08:42 -05:00
Jeff Avallone
97a61dc0f3 Adding prod and dev webpack configs 2018-02-10 16:05:25 -05:00
Jeff Avallone
d78f4efd16 Base webpack config 2018-02-10 15:58:54 -05:00
Jeff Avallone
c67101a209 Updating .gitignore 2018-02-10 15:58:54 -05:00
Jeff Avallone
ae2b7c74dd Clearing out old site code 2018-02-10 13:33:13 -05:00
192 changed files with 11041 additions and 11980 deletions

View File

@ -1,3 +0,0 @@
{
"presets": ["es2015"]
}

View File

@ -1,17 +0,0 @@
{
"projects": {
"default": "regexper"
},
"targets": {
"regexper": {
"hosting": {
"production": [
"regexper"
],
"preview": [
"regexper-preview"
]
}
}
}
}

36
.gitignore vendored
View File

@ -1,7 +1,29 @@
node_modules
.sass-cache
build
docs
tmp
.firebase/
firebase-debug.log
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Dependency directories
node_modules/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Yarn Integrity file
.yarn-integrity
# Build output
build/
script/__build__/
# Coverage reports
coverage/
# Favicon cache
.wwp-cache/

View File

@ -1,73 +1,102 @@
image: node:8
image: node:latest
stages:
- setup
- test
- build
- deploy
.shared_runner: &shared_runner
tags:
- shared
.cache_consumer: &cache_consumer
cache:
policy: pull
paths:
- node_modules
.preview_job: &preview_job
only:
- master
- react # TODO: Change to master once merged
.production_job: &production_job
only:
- /^release-.*$/
.build_template: &build_template
<<: *shared_runner
<<: *cache_consumer
stage: build
dependencies: []
artifacts:
paths:
- build/
script:
- yarn gulp build
- rm -r build/__discard__
- gzip -k -6 $(find build/ -type f)
- yarn build
.deploy_template: &deploy_template
<<: *shared_runner
<<: *cache_consumer
stage: deploy
script:
- yarn firebase use --token $FIREBASE_DEPLOY_KEY default
- yarn firebase deploy --only hosting:$DEPLOY_ENV -m "Pipeline $CI_PIPELINE_ID, Build $CI_BUILD_ID" --non-interactive --token $FIREBASE_DEPLOY_KEY
- yarn deploy
cache:
paths:
- node_modules/
setup:
<<: *shared_runner
stage: setup
cache:
paths:
- node_modules
script:
- yarn install
before_script:
- yarn install
test:
<<: *shared_runner
<<: *cache_consumer
stage: test
coverage: '/^Statements\s*:\s*([^%]+)/'
artifacts:
paths:
- coverage/
script:
- yarn test
build-preview:
build_preview:
<<: *build_template
<<: *preview_job
variables:
BANNER: preview
DEPLOY_ENV: preview
GA_PROP: $PREVIEW_GA_PROPERTY
GA_PROPERTY: $PREVIEW_GA_PROPERTY
build-production:
build_production:
<<: *build_template
<<: *production_job
variables:
DEPLOY_ENV: production
GA_PROP: $PROD_GA_PROPERTY
GA_PROPERTY: $PROD_GA_PROPERTY
deploy-preview:
deploy_preview:
<<: *deploy_template
<<: *preview_job
dependencies:
- build-preview
- build_preview
environment:
name: preview
url: https://preview.regexper.com
variables:
DEPLOY_ENV: preview
CLOUD_FRONT_ID: $PREVIEW_CLOUDFRONT_ID
DEPLOY_BUCKET: $PREVIEW_DEPLOY_BUCKET
deploy-production:
deploy_production:
<<: *deploy_template
<<: *production_job
dependencies:
- build-production
- build_production
environment:
name: production
url: https://regexper.com
variables:
DEPLOY_ENV: production
CLOUD_FRONT_ID: $PROD_CLOUDFRONT_ID
DEPLOY_BUCKET: $PROD_DEPLOY_BUCKET

68
.jscsrc
View File

@ -1,68 +0,0 @@
{
"requireCurlyBraces": [
"if",
"else",
"for",
"while",
"do",
"try",
"catch"
],
"requireSpaceAfterKeywords": [
"if",
"else",
"for",
"while",
"do",
"switch",
"case",
"return",
"try",
"typeof"
],
"requireSpaceBeforeBlockStatements": true,
"requireParenthesesAroundIIFE": true,
"requireSpacesInConditionalExpression": true,
"disallowSpacesInNamedFunctionExpression": {
"beforeOpeningRoundBrace": true
},
"disallowSpacesInFunctionDeclaration": {
"beforeOpeningRoundBrace": true
},
"requireSpaceBetweenArguments": true,
"requireMultipleVarDecl": "onevar",
"requireVarDeclFirst": true,
"requireBlocksOnNewline": true,
"disallowEmptyBlocks": true,
"disallowSpacesInsideArrayBrackets": true,
"disallowSpacesInsideParentheses": true,
"requireCommaBeforeLineBreak": true,
"disallowSpaceAfterPrefixUnaryOperators": true,
"disallowSpaceBeforePostfixUnaryOperators": true,
"disallowSpaceBeforeBinaryOperators": [
","
],
"requireSpacesInForStatement": true,
"requireSpacesInAnonymousFunctionExpression": {
"beforeOpeningCurlyBrace": true
},
"requireSpaceBeforeBinaryOperators": true,
"requireSpaceAfterBinaryOperators": true,
"disallowKeywords": [
"with",
"continue"
],
"validateIndentation": 2,
"disallowMixedSpacesAndTabs": true,
"disallowTrailingWhitespace": true,
"disallowTrailingComma": true,
"disallowKeywordsOnNewLine": [
"else"
],
"requireLineFeedAtFileEnd": true,
"requireCapitalizedConstructors": true,
"requireDotNotation": true,
"disallowNewlineBeforeBlockStatements": true,
"disallowMultipleLineStrings": true,
"requireSpaceBeforeObjectValues": true
}

View File

@ -1,10 +0,0 @@
language: node_js
node_js:
- "node"
addons:
firefox: "latest"
before_script:
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"
- sleep 3
script: yarn test

1
.yarnrc Normal file
View File

@ -0,0 +1 @@
registry "https://registry.yarnpkg.com"

View File

@ -20,13 +20,42 @@ To start a development server, run:
$ yarn start
This will build the site into the ./build directory, start a local start on port 8080, and begin watching the source files for modifications. The site will automatically be rebuilt when files are changed. Also, if you browser has the LiveReload extension, then the page will be reloaded.
### Translating
These other gulp tasks are available:
A helper tool is available to support maintaining translations. Running `yarn i18n:scrub` will update locale data files under `src/locales` by normalizing YAML syntax across all the files, maintaining a `missing.yaml` file under each locale, and indicating any existing translations that appear to be no longer used. For this, the `src/locales/en/translation.yaml` file is used as a source of truth.
$ gulp docs # Build documentation into the ./docs directory
$ gulp build # Build the site into the ./build directory
$ yarn test # Run JSCS lint and Karma tests
To add a new locale, first create a directory for the locale under `src/locales` (no files need to be added at this point), then run `yarn i18n:scrub`. This will create a `src/locales/<locale name>/missing.yaml` file that contains required translations. Add a `src/locales/<locale name>/translation.yaml` file and at a minimum add a translation for the `/displayName` value. This should be the translated name of the locale your are adding.
If you are looking to translation missing text for a locale that is already present in the project, start by running `yarn i18n:scrub`. Check the `src/locales/<locale name>/missing.yaml` file for translations that are needed. Add any updated translations to `src/locales/<locale name>/translation.yaml`.
Before committing any updated translations, run `yarn i18n:scrub` again to maintain syntax in the YAML files and remove any translated entries from `missing.yaml`. Commit any changes to the `translation.yaml` and `missing.yaml` files.
### Available scripts
* `yarn start` - Start a development server on port 8080
* `yarn start:prod` - Run a build and start a web server on port 8080. This will not automatically rebuild.
* `yarn build` - Run a production build (used for deployments and for rebuilding when running `yarn start:prod`)
* `yarn deploy` - Deploy application to AWS S3 bucket
* `yarn test` - Run lint and unit tests
* `yarn test:lint` - Run eslint
* `yarn test:unit` - Run jest unit tests
* `yarn test:watch` - Run jest in watch mode
* `yarn test:bundle-analyzer` - Generate webpack-bundle-analyzer report
* `yarn i18n:scrub` - Scrubs i18n locale configs. Adds missing keys and normalizes YAML formatting
## Configuration
Several environment variables are used to configure the application at build-time. None of these values are required during testing.
* `NODE_ENV` - Effects build-time optimizations. Set to `"production"` for builds and unit tests in package.json. Setting to anything else will show a banner in the application's header.
* `GA_PROPERTY` - Google Analytics property ID.
* `SENTRY_KEY` - Sentry.io DSN key for error reporting.
* `CI_COMMIT_REF_SLUG`, `CI_COMMIT_SHA` - GitLab CI values used to generate build ID. Displayed in application footer and used in Sentry.io error reports.
* `CLOUD_FRONT_ID` - AWS CloudFront distribution ID to invalidating when running `yarn deploy`
* `DEPLOY_BUCKET` - AWS S3 bucket to deploy application to when running `yarn deploy`.
* `DEPLOY_ENV` - Environment the application will be deployed to. Used to report environment in Sentry.io error reports. Typically set to either "preview" or "production".
* `BANNER` - Text to display in header banner. Generally generated from `NODE_ENV`
* `BUILD_ID` - Application build ID. Generated from `CI_COMMIT_REF_SLUG` and `CI_COMMIT_SHA` if not set.
## License

View File

@ -1,31 +0,0 @@
var path = require('path'),
_ = require('lodash'),
buildRoot = process.env.BUILD_PATH || path.join(__dirname, './build'),
buildPath = _.bind(path.join, path, buildRoot);
module.exports = {
buildRoot: buildRoot,
buildPath: buildPath,
globs: {
other: './src/**/*.!(hbs|scss|js|peg)',
templates: './src/**/*.hbs',
data: ['./lib/data/**/*.json', './lib/data/**/*.js'],
helpers: './lib/helpers/**/*.js',
partials: './lib/partials/**/*.hbs',
sass: './src/**/*.scss',
svg_sass: './src/sass/svg.scss',
js: ['./src/**/*.js', './src/**/*.peg'],
spec: './spec/**/*_spec.js',
lint: [
'./lib/**/*.js',
'./src/**/*.js',
'./spec/**/*.js',
'./*.js'
]
},
lintRoots: ['lib', 'src', 'spec'],
browserify: {
debug: true,
fullPaths: false
}
};

21
deploy.config.js Normal file
View File

@ -0,0 +1,21 @@
const path = require('path');
module.exports = {
s3Bucket: process.env.DEPLOY_BUCKET,
cloudFrontId: process.env.CLOUD_FRONT_ID,
deployFrom: path.resolve(__dirname, 'build'),
paths: [
{
match: /^service-worker.js/,
CacheControl: 'max-age=0'
},
{
match: /^(js|css|icons-\w{8})/,
CacheControl: 'public, max-age=31536000'
},
{ // Default config. MUST BE LAST
match: /./,
CacheControl: 'public, max-age=96400'
}
]
};

View File

@ -1,12 +0,0 @@
{
"hosting": [
{
"target": "production",
"public": "build"
},
{
"target": "preview",
"public": "build"
}
]
}

View File

@ -1,101 +0,0 @@
const gulp = require('gulp-help')(require('gulp')),
_ = require('lodash'),
notify = require('gulp-notify'),
folderToc = require('folder-toc'),
docco = require('gulp-docco'),
connect = require('gulp-connect'),
hb = require('gulp-hb'),
frontMatter = require('gulp-front-matter'),
rename = require('gulp-rename'),
config = require('./config'),
gutil = require('gulp-util'),
webpack = require('webpack')
webpackConfig = require('./webpack.config'),
fs = require('fs');
gulp.task('default', 'Auto-rebuild site on changes.', ['server', 'docs'], function() {
gulp.watch(config.globs.other, ['static']);
gulp.watch(_.flatten([
config.globs.templates,
config.globs.data,
config.globs.helpers,
config.globs.partials,
config.globs.svg_sass
]), ['markup']);
gulp.watch(_.flatten([
config.globs.sass,
config.globs.js
]), ['webpack']);
gulp.watch(config.globs.js, ['docs']);
});
gulp.task('docs', 'Build documentation into ./docs directory.', ['docs:files'], function() {
folderToc('./docs', {
filter: '*.html'
});
});
gulp.task('docs:files', false, function() {
return gulp.src(config.globs.js)
.pipe(docco())
.pipe(gulp.dest('./docs'));
});
gulp.task('server', 'Start development server.', ['build'], function() {
gulp.watch(config.buildPath('**/*'), function(file) {
return gulp.src(file.path).pipe(connect.reload());
});
return connect.server({
root: config.buildRoot,
livereload: true
});
});
gulp.task('build', 'Build site into ./build directory.', ['static', 'webpack', 'markup']);
gulp.task('static', 'Build static files into ./build directory.', function() {
return gulp.src(config.globs.other, { base: './src' })
.pipe(gulp.dest(config.buildRoot));
});
gulp.task('markup', 'Build markup into ./build directory.', ['webpack'], function() {
var hbStream = hb({
data: config.globs.data,
helpers: config.globs.helpers,
partials: config.globs.partials,
parsePartialName: function(option, file) {
return _.last(file.path.split(/\\|\//)).replace('.hbs', '');
},
bustCache: true
});
hbStream.partials({
svg_styles: fs.readFileSync(config.buildRoot + '/css/svg.css').toString()
});
if (process.env.GA_PROP) {
hbStream.data({
'gaPropertyId': process.env.GA_PROP
});
}
if (process.env.SENTRY_KEY) {
hbStream.data({
'sentryKey': process.env.SENTRY_KEY
});
}
return gulp.src(config.globs.templates)
.pipe(frontMatter())
.pipe(hbStream)
.on('error', notify.onError())
.pipe(rename({ extname: '.html' }))
.pipe(gulp.dest(config.buildRoot));
});
gulp.task('webpack', 'Build JS & CSS into ./build directory.', function(callback) {
webpack(webpackConfig, function(err, stats) {
if (err) {
throw new gutil.PluginError('webpack', err);
}
gutil.log('[webpack]', stats.toString());
callback();
});
});

View File

@ -1,35 +0,0 @@
module.exports = function(karma) {
karma.set({
frameworks: ['jasmine'],
files: [ 'spec/test_index.js' ],
preprocessors: {
'spec/test_index.js': ['webpack', 'sourcemap']
},
reporters: ['progress', 'notify'],
colors: true,
logLevel: karma.LOG_INFO,
browsers: ['Firefox'],
autoWatch: true,
singleRun: false,
webpack: {
devtool: 'inline-source-map',
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
{
test: require.resolve('snapsvg'),
loader: 'imports-loader?this=>window,fix=>module.exports=0'
},
{
test: /\.peg$/,
loader: require.resolve('./lib/canopy-loader')
}
]
}
}
});
};

View File

@ -1,6 +0,0 @@
var canopy = require('canopy');
module.exports = function(source) {
this.cacheable();
return canopy.compile(source);
};

View File

@ -1,186 +0,0 @@
[
{
"label": "June 4, 2018 Release",
"changes": [
"Moving source to GitLab and updating some links on the site."
]
},
{
"label": "May 24, 2018 Release",
"changes": [
"Supporting browser \"Do Not Track\" setting. When enabled, this will prevent use of Google Analytics and Sentry error reporting."
]
},
{
"label": "February 10, 2018 Release",
"changes": [
"Adding 'sticky' and 'unicode' flag support",
"Encoding parenthesis in the permalink and browser URLs",
"Adding PNG download support"
]
},
{
"label": "July 31, 2016 Release",
"changes": [
"Merged code to enable automated testing with Travis CI from <a href=\"https://github.com/Byron\">Sebastian Thiel</a>",
"Merged feature to show an informational tooltip on loop labels from <a href=\"https://github.com/ThibWeb\">Thibaud Colas</a>",
"Fixed issue with '^' and '$' not being allowed in the middle of a fragment (see <a href=\"https://github.com/javallone/regexper-static/issues/29\">GitHub issue</a>)",
"Updating several dependencies",
"Some stylistic code cleanup"
]
},
{
"label": "May 31, 2016 Release",
"changes": [
"Putting separate CSS for generated SVG images back into the build. Downloaded images have been broken since the March 10 release because the SVG styles were merged into the page styles."
]
},
{
"label": "May 23, 2016 Release",
"changes": [
"Refactored tracking code to support latest Google Analytics setup"
]
},
{
"label": "March 10, 2016 Release",
"changes": [
"Embedding SVG icon images into markup",
"Some changes for minor performance improvements",
"Updating several dependencies"
]
},
{
"label": "March 8, 2016 Release",
"changes": [
"Replaced icon font with individual SVG images"
]
},
{
"label": "March 3, 2016 Release",
"changes": [
"Merged some code cleanup and a bugfix from <a href=\"https://github.com/Byron\">Sebastian Thiel</a>",
"Updated notice for IE8 users to no longer include link to legacy site"
]
},
{
"label": "December 21, 2015 Release",
"changes": [
"Updating NPM dependencies to fix JS error that only appeared when running site from a local development environment (see <a href=\"https://github.com/javallone/regexper-static/issues/21\">GitHub issue</a>)"
]
},
{
"label": "November 10, 2015 Release",
"changes": [
"Fixing Babel integration to include polyfills"
]
},
{
"label": "November 1, 2015 Release",
"changes": [
"Switching from Compass to node-sass and Bourbon (no more need for Ruby)",
"Switching to Babel instead of es6ify",
"Improving sourcemap generation",
"Cleanup of the build process"
]
},
{
"label": "October 31, 2015 Release",
"changes": [
"Reducing file size for title font",
"Cleaning up gulpfile",
"Upgrading most dependencies",
"Switching to Handlebars for template rendering"
]
},
{
"label": "September 17, 2015 Release",
"changes": [
"Fixing styling of labels on repetitions",
"Fixing issue with vertical centering of alternation expressions that include empty expressions (see <a href=\"https://github.com/javallone/regexper-static/pull/16\">GitHub issue</a>)"
]
},
{
"label": "September 2, 2015 Release",
"changes": [
"Merging fix for error reporting from (see <a href=\"https://github.com/javallone/regexper-static/pull/15\">GitHub pull request</a>)"
]
},
{
"label": "July 5, 2015 Release",
"changes": [
"Updating Creative Commons license badge URL so it isn't pointing to a redirecting URL anymore"
]
},
{
"label": "June 22, 2015 Release",
"changes": [
"Tweaking buggy Firefox hash detection code based on JavaScript errors that were logged"
]
},
{
"label": "June 16, 2015 Release",
"changes": [
"Fixes issue with expressions containing a \"%\" not rendering in Firefox (see <a href=\"https://github.com/javallone/regexper-static/issues/12\">GitHub issue</a>)",
"Fixed rendering in IE that was causing \"--&gt;\" to display at the top of the page."
]
},
{
"label": "April 14, 2015 Release",
"changes": [
"Rendering speed improved. Most users will probably not see much improvement since logging data indicates that expressing rendering time is typically less than 1 second. Using the <a href=\"http://www.ex-parrot.com/pdw/Mail-RFC822-Address.html\">RFC822 email regular expression</a> though shows a rendering speed improvement from ~120 seconds down to ~80 seconds.",
"Fixing a bug that would only occur when attempting to render an expression while another is in the process of rendering"
]
},
{
"label": "March 14, 2015 Release",
"changes": [
"Removing use of Q for promises in favor of \"native\" ES6 promises (even though they aren't quite native everywhere yet)"
]
},
{
"label": "March 13, 2015 Release",
"changes": [
"Fixes bug with numbering of nested subexpressions (see <a href=\"https://github.com/javallone/regexper-static/issues/7\">GitHub issue</a>)"
]
},
{
"label": "February 11, 2015 Release",
"changes": [
"Various adjustments to analytics: tracking expression rendering time and JS errors",
"Escape sequences that match to a specific character now display their hexadecimal code (actually done on January 25, but I forgot to update the changelog)",
"Fixing styling issue with header links (see <a href=\"https://github.com/javallone/regexper-static/issues/5\">GitHub issue</a>)"
]
},
{
"label": "December 30, 2014 Release",
"changes": [
"Fixing bug that prevented rendering empty subexpressions",
"Fixing minor styling bug when permalink is disabled",
"Cleaning up some duplicated styles and JS"
]
},
{
"label": "December 29, 2014 Release",
"changes": [
"Tweaking analytics data to help with addressing issues in deployed code (work will likely continue on this)",
"Added progress bars on the documentation page",
"Removed the loading spinner everywhere",
"Animated the progress bars"
]
},
{
"label": "December 26, 2014 Release",
"changes": [
"Freshened up design",
"Multiline regular expression input field (press Shift-Enter to render)",
"Added a changelog",
"Added documentation",
"All parsing and rendering happens client-side (using <a href=\"http://canopy.jcoglan.com/\">Canopy</a> and <a href=\"http://snapsvg.io/\">Snap.svg</a>)",
"Added Download link (not available in older browsers)",
"Added display of regular expression flags (ignore case, global, multiline)",
"Added indicator of quantifier greedy-ness",
"Various improvements to parsing of regular expression",
"Rendering of a regular expression can be canceled by pressing Escape"
]
}
]

View File

@ -1 +0,0 @@
module.exports = new Date().toISOString();

View File

@ -1,5 +0,0 @@
module.exports.register = function(handlebars) {
handlebars.registerHelper('icon', function(selector, context) {
return new handlebars.SafeString(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 8 8"><use xlink:href="${selector}" /></svg>`);
});
};

View File

@ -1,5 +0,0 @@
var layouts = require('handlebars-layouts');
module.exports.register = function(handlebars) {
layouts.register(handlebars);
};

View File

@ -1,15 +0,0 @@
{{#if gaPropertyId}}
<script>
if (navigator.doNotTrack !== '1' && window.doNotTrack !== '1') {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '{{{gaPropertyId}}}', 'auto');
ga('send', 'pageview');
} else {
console.log('Google Analytics disabled by "Do Not Track"');
}
</script>
{{/if}}

View File

@ -1,68 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>Regexper{{#if file.frontMatter.title}} - {{file.frontMatter.title}}{{/if}}</title>
<meta name="description" content="Regular expression visualizer using railroad diagrams" />
<meta name="viewport" content="width=device-width" />
<meta name="theme-color" content="#bada55" />
{{> "google_analytics"}}
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="author" href="humans.txt" />
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Bangers&text=Regxpr" />
<link rel="stylesheet" href="/css/main.css" />
<!-- Built: {{date}} -->
</head>
<body>
<header>
<div class="logo">
<h1><a href="/">Regexper</a></h1>
<!-- n. One who regexpes -->
<span>You thought you only had two problems&hellip;</span>
</div>
<nav>
<ul>
<li>
<a class="inline-icon" href="/changelog.html">{{icon "#list-rich"}}Changelog</a>
</li>
<li>
<a class="inline-icon" href="/documentation.html">{{icon "#document"}}Documentation</a>
</li>
<li>
<a class="inline-icon" href="https://gitlab.com/javallone/regexper-static">{{icon "#code"}}Source on GitLab</a>
</li>
</ul>
</nav>
</header>
<main id="content">
{{#block "body"}}{{/block}}
</main>
<footer>
{{#block "footer"}}
<ul class="inline-list">
<li>Created by <a href="mailto:jeff.avallone@gmail.com">Jeff Avallone</a></li>
<li>
Generated images licensed:
<a rel="license" href="http://creativecommons.org/licenses/by/3.0/"><img alt="Creative Commons License" src="https://licensebuttons.net/l/by/3.0/80x15.png" /></a>
</li>
</ul>
<script type="text/html" id="svg-container-base">
{{> "svg_template"}}
</script>
{{> "sentry"}}
{{/block}}
</footer>
{{> "open_iconic"}}
</body>
</html>

View File

@ -1,23 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" id="open-iconic">
<!-- These icon are from the Open Iconic project https://useiconic.com/open/ -->
<defs>
<g id="code">
<path d="M5 0l-3 6h1l3-6h-1zm-4 1l-1 2 1 2h1l-1-2 1-2h-1zm5 0l1 2-1 2h1l1-2-1-2h-1z" transform="translate(0 1)" />
</g>
<g id="data-transfer-download">
<path d="M3 0v3h-2l3 3 3-3h-2v-3h-2zm-3 7v1h8v-1h-8z" />
</g>
<g id="document">
<path d="M0 0v8h7v-4h-4v-4h-3zm4 0v3h3l-3-3zm-3 2h1v1h-1v-1zm0 2h1v1h-1v-1zm0 2h4v1h-4v-1z" />
</g>
<g id="link-intact">
<path d="M5.88.03c-.18.01-.36.03-.53.09-.27.1-.53.25-.75.47a.5.5 0 1 0 .69.69c.11-.11.24-.17.38-.22.35-.12.78-.07 1.06.22.39.39.39 1.04 0 1.44l-1.5 1.5c-.44.44-.8.48-1.06.47-.26-.01-.41-.13-.41-.13a.5.5 0 1 0-.5.88s.34.22.84.25c.5.03 1.2-.16 1.81-.78l1.5-1.5c.78-.78.78-2.04 0-2.81-.28-.28-.61-.45-.97-.53-.18-.04-.38-.04-.56-.03zm-2 2.31c-.5-.02-1.19.15-1.78.75l-1.5 1.5c-.78.78-.78 2.04 0 2.81.56.56 1.36.72 2.06.47.27-.1.53-.25.75-.47a.5.5 0 1 0-.69-.69c-.11.11-.24.17-.38.22-.35.12-.78.07-1.06-.22-.39-.39-.39-1.04 0-1.44l1.5-1.5c.4-.4.75-.45 1.03-.44.28.01.47.09.47.09a.5.5 0 1 0 .44-.88s-.34-.2-.84-.22z" />
</g>
<g id="list-rich">
<path d="M0 0v3h3v-3h-3zm4 0v1h4v-1h-4zm0 2v1h3v-1h-3zm-4 2v3h3v-3h-3zm4 0v1h4v-1h-4zm0 2v1h3v-1h-3z" />
</g>
<g id="warning">
<path d="M3.09 0c-.06 0-.1.04-.13.09l-2.94 6.81c-.02.05-.03.13-.03.19v.81c0 .05.04.09.09.09h6.81c.05 0 .09-.04.09-.09v-.81c0-.05-.01-.14-.03-.19l-2.94-6.81c-.02-.05-.07-.09-.13-.09h-.81zm-.09 3h1v2h-1v-2zm0 3h1v1h-1v-1z" />
</g>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,17 +0,0 @@
{{#if sentryKey}}
<script>
if (navigator.doNotTrack !== '1' && window.doNotTrack !== '1') {
window.SENTRY_SDK = {
url: 'https://cdn.ravenjs.com/3.25.2/raven.min.js',
dsn: '{{{sentryKey}}}',
options: {
whitelistUrls: [/https:\/\/(.*\.)?regexper\.com/]
}
};
(function(a,b,g,e,h){var k=a.SENTRY_SDK,f=function(a){f.data.push(a)};f.data=[];var l=a[e];a[e]=function(c,b,e,d,h){f({e:[].slice.call(arguments)});l&&l.apply(a,arguments)};var m=a[h];a[h]=function(c){f({p:c.reason});m&&m.apply(a,arguments)};var n=b.getElementsByTagName(g)[0];b=b.createElement(g);b.src=k.url;b.crossorigin="anonymous";b.addEventListener("load",function(){try{a[e]=l;a[h]=m;var c=f.data,b=a.Raven;b.config(k.dsn,k.options).install();var g=a[e];if(c.length)for(var d=0;d<c.length;d++)c[d].e?g.apply(b.TraceKit,c[d].e):c[d].p&&b.captureException(c[d].p)}catch(p){console.log(p)}});n.parentNode.insertBefore(b,n)})(window,document,"script","onerror","onunhandledrejection");
} else {
console.log('Sentry error logging disabled by "Do Not Track"');
}
</script>
{{/if}}

View File

@ -1,25 +0,0 @@
<div class="svg">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
version="1.1">
<defs>
<style type="text/css">{{> svg_styles}}</style>
</defs>
<metadata>
<rdf:RDF>
<cc:License rdf:about="http://creativecommons.org/licenses/by/3.0/">
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
</cc:License>
</rdf:RDF>
</metadata>
</svg>
</div>
<div class="progress">
<div style="width:0;"></div>
</div>

View File

@ -1,7 +1,7 @@
{
"name": "regexper",
"version": "1.0.0",
"description": "Regular expression visualization tool",
"description": "Regular expression visualization tool using railroad diagrams",
"homepage": "http://regexper.com",
"author": {
"name": "Jeffrey Avallone",
@ -10,49 +10,177 @@
"license": "MIT",
"private": true,
"scripts": {
"pretest": "jscs lib/ src/ spec/",
"test": "karma start --single-run",
"build": "gulp build",
"start": "gulp"
"start": "webpack-dev-server --config webpack.dev.js",
"start:prod": "run-s build start:http-server",
"start:http-server": "http-server -c0 ./build",
"build": "cross-env NODE_ENV=production run-s build:webpack:web build:webpack:prerender build:prerender",
"build:webpack:web": "webpack --config webpack.prod-web.js",
"build:webpack:prerender": "webpack --config webpack.prod-prerender.js",
"build:prerender": "node ./script/__build__/prerender.js",
"deploy": "node ./script/s3-upload.js deploy.config.js",
"test": "run-s test:lint 'test:unit --coverage'",
"test:unit": "cross-env NODE_ENV=production jest",
"test:lint": "eslint --ignore-path .gitignore .",
"test:watch": "yarn test:unit --watch",
"test:bundle-analyzer": "cross-env NODE_ENV=production webpack --config webpack.bundle-analyzer.js",
"i18n:scrub": "node ./script/i18n-scrub.js",
"precommit": "run-s test:lint"
},
"browserslist": [
">1%",
"not ie < 11"
],
"babel": {
"presets": [
"env",
"react"
],
"plugins": [
"transform-runtime",
"transform-class-properties",
"transform-object-rest-spread",
"transform-decorators-legacy",
"syntax-dynamic-import"
]
},
"postcss": {
"plugins": {
"postcss-import": {},
"postcss-cssnext": {
"features": {
"rem": false
}
}
}
},
"jest": {
"clearMocks": true,
"setupTestFrameworkScriptFile": "<rootDir>/src/setup/jest.js",
"snapshotSerializers": [
"enzyme-to-json/serializer"
],
"modulePaths": [
"src",
"node_modules"
],
"moduleNameMapper": {
"\\.svg$": "__mocks__/svgMock.js",
"\\.css$": "identity-obj-proxy"
},
"collectCoverageFrom": [
"src/**/*.js",
"!src/i18n.js",
"!src/prerender.js",
"!src/setup/service-worker.js",
"!src/setup/jest.js",
"!src/pages/**/config.js",
"!src/pages/**/browser.js"
],
"coverageReporters": [
"text-summary",
"html"
]
},
"eslintConfig": {
"env": {
"browser": true,
"es6": true,
"node": true,
"jest/globals": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"parser": "babel-eslint",
"parserOptions": {
"sourceType": "module"
},
"plugins": [
"react",
"jest"
],
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
},
"dependencies": {
"@alienfast/i18next-loader": "^1.0.14",
"aws-sdk": "^2.247.1",
"babel-core": "^6.26.0",
"babel-eslint": "^8.2.1",
"babel-jest": "^23.0.1",
"babel-loader": "^7.1.2",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"babel-register": "^6.26.0",
"cheerio": "^1.0.0-rc.2",
"colors": "^1.1.2",
"copy-webpack-plugin": "^4.4.1",
"cross-env": "^5.1.3",
"css-loader": "^0.28.9",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"enzyme-to-json": "^3.3.1",
"eslint": "^4.17.0",
"eslint-plugin-jest": "^21.8.0",
"eslint-plugin-react": "^7.6.1",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"feather-icons": "^4.5.0",
"html-webpack-plugin": "^3.0.0",
"i18next": "^11.3.2",
"i18next-browser-languagedetector": "^2.1.0",
"identity-obj-proxy": "^3.0.0",
"immutable": "^3.8.2",
"jest": "^23.1.0",
"mime-types": "^2.1.18",
"npm-run-all": "^4.1.2",
"postcss-cssnext": "^3.1.0",
"postcss-import": "^11.1.0",
"postcss-loader": "^2.1.0",
"raven-js": "^3.22.2",
"react": "^16.3.0",
"react-dom": "^16.3.0",
"react-ga": "^2.4.1",
"react-i18next": "^7.3.6",
"react-test-renderer": "^16.3.0",
"recursive-readdir": "^2.2.2",
"style-loader": "^0.21.0",
"svg-react-loader": "^0.4.5",
"uglifyjs-webpack-plugin": "^1.1.8",
"url-search-params": "^0.10.0",
"webapp-webpack-plugin": "^2.1.0",
"webpack": "^4.1.1",
"webpack-cli": "^3.0.1",
"webpack-merge": "^4.1.1",
"webpack-node-externals": "^1.6.0",
"workbox-webpack-plugin": "^3.0.1"
},
"devDependencies": {
"babel-core": "^6.17.0",
"babel-loader": "^7.1.1",
"babel-polyfill": "^6.3.14",
"babel-preset-es2015": "^6.16.0",
"babel-runtime": "^6.3.19",
"canopy": "^0.2.0",
"css-loader": "^0.28.4",
"docco": "^0.7.0",
"extract-loader": "^1.0.0",
"file-loader": "^0.11.2",
"firebase-tools": "^6.2.2",
"folder-toc": "^0.1.0",
"gulp": "^3.8.10",
"gulp-connect": "^5.0.0",
"gulp-docco": "0.0.4",
"gulp-front-matter": "^1.3.0",
"gulp-hb": "^6.0.2",
"gulp-help": "^1.6.1",
"gulp-notify": "^3.0.0",
"gulp-rename": "^1.2.2",
"gulp-util": "^3.0.7",
"handlebars-layouts": "^3.1.2",
"imports-loader": "^0.7.1",
"jasmine-core": "^2.4.1",
"jscs": "^3.0.7",
"karma": "^1.1.2",
"karma-firefox-launcher": "^1.0.0",
"karma-jasmine": "^1.0.2",
"karma-notify-reporter": "^1.0.1",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^2.0.4",
"lodash": "^4.6.1",
"node-bourbon": "^4.2.3",
"node-sass": "^4.5.3",
"sass-loader": "^6.0.6",
"snapsvg": "^0.5.1",
"watchify": "^3.7.0",
"webpack": "^3.4.1"
"http-server": "^0.11.1",
"husky": "^0.14.3",
"js-yaml": "^3.10.0",
"webpack-bundle-analyzer": "^2.11.1",
"webpack-dev-server": "^3.1.1"
}
}

View File

@ -13,4 +13,4 @@
# TECHNOLOGY COLOPHON
HTML5, CSS3, SVG, Sass, Open Iconic
HTML5, CSS3, SVG, React, Feather Icons

102
script/i18n-scrub.js Normal file
View File

@ -0,0 +1,102 @@
const util = require('util');
const path = require('path');
const fs = require('fs');
const yaml = require('js-yaml');
const colors = require('colors/safe');
const readdir = util.promisify(fs.readdir);
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
const localesDir = path.resolve(__dirname, '../src/locales');
const loadLocales = async () => {
const languages = (await readdir(localesDir)).filter(name => name !== 'index.js');
let localeData = {};
await Promise.all(languages.map(async lang => {
const langDir = path.resolve(localesDir, lang);
const namespaces = (await readdir(langDir))
.filter(file => /\.yaml$/.test(file));
localeData[lang] = {};
await Promise.all(namespaces.map(async ns => {
const nsFile = path.resolve(langDir, ns);
localeData[lang][ns.replace('.yaml', '')] = yaml.safeLoad(await readFile(nsFile));
}));
}));
return localeData;
};
const saveLocales = async locales => {
await Promise.all(Object.keys(locales).map(async langName => {
const lang = locales[langName];
await Promise.all(Object.keys(lang).map(async nsName => {
const nsFile = path.resolve(localesDir, langName, `${ nsName }.yaml`);
const yamlDump = yaml.safeDump(lang[nsName], {
sortKeys: true
});
await writeFile(nsFile, yamlDump);
}));
}));
};
loadLocales()
.then(async locales => {
const sourceLocale = locales.en.translation;
const requiredKeys = Object.keys(sourceLocale);
const languages = Object.keys(locales).filter(lang => lang !== 'en');
languages.forEach(langName => {
const lang = locales[langName];
const presentKeys = Object.keys(lang).filter(nsName => nsName !== 'missing').reduce((list, nsName) => {
return list.concat(Object.keys(lang[nsName]));
}, []);
const missingKeys = requiredKeys.filter(key => !presentKeys.includes(key));
const extraKeys = presentKeys.filter(key => !requiredKeys.includes(key));
if (!lang.translation) {
lang.translation = {};
}
if (!lang.missing) {
lang.missing = {};
}
missingKeys.forEach(key => {
if (lang.missing[key]) {
return;
}
console.log(colors.yellow.bold('MISSING:'), `${ langName } needs value for "${ colors.bold(key) }".`); //eslint-disable-line no-console
lang.missing[key] = sourceLocale[key];
});
presentKeys.forEach(key => {
if (!lang.missing[key]) {
return;
}
console.log(colors.yellow.bold('DEFINED:'), `Removing "${ colors.bold(key) }" from ${ langName}.missing. It is defined elsewhere.`); // eslint-disable-line no-console
delete lang.missing[key];
});
extraKeys.forEach(key => {
console.log(colors.yellow.bold('EXTRA:'), `${ langName } has extra key for "${ colors.bold(key) }". It should be removed.`); // eslint-disable-line no-console
});
});
return locales;
})
.then(saveLocales)
.then(() => {
console.log('Done updating locales'); // eslint-disable-line no-console
})
.catch(e => {
console.error(colors.red.bold('FAILED:'), e); // eslint-disable-line no-console
process.exit(1);
});

35
script/prerender.js Normal file
View File

@ -0,0 +1,35 @@
// NOTE: This script *MUST* be built with webpack since it requires React
// components. The script is built and run as part of `yarn build`
import React from 'react';
import { renderToString } from 'react-dom/server';
import fs from 'fs';
import util from 'util';
import cheerio from 'cheerio';
import colors from 'colors/safe';
import 'i18n';
const readdir = util.promisify(fs.readdir);
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
readdir('./src/pages')
.then(pages => (
Promise.all(pages.map(async page => {
const Component = (await import(`pages/${ page }/Component`)).default;
const pagePath = `./build/${ page }.html`;
const markup = cheerio.load(await readFile(pagePath));
markup('#root').html(renderToString(<Component/>));
await writeFile(pagePath, markup.html());
console.log(colors.green.bold('PRERENDERED:'), `${ page }.html`); // eslint-disable-line no-console
}))
))
.then(() => console.log('Done prerendering')) // eslint-disable-line no-console
.catch(e => {
console.error(colors.red.bold('FAIL:'), e); // eslint-disable-line no-console
process.exit(1);
});

101
script/s3-upload.js Normal file
View File

@ -0,0 +1,101 @@
/* eslint-disable no-console */
const path = require('path');
const fs = require('fs');
const colors = require('colors/safe');
const readdir = require('recursive-readdir');
const AWS = require('aws-sdk');
const mime = require('mime-types');
const s3 = new AWS.S3();
const cloudFront = new AWS.CloudFront();
const config = require(path.resolve(process.argv[2]));
const configFor = path => {
const { match, ...conf } = config.paths.find(conf => conf.match.test(path)); // eslint-disable-line no-unused-vars
return {
ContentType: mime.lookup(path) || 'application/octet-stream',
...conf
};
};
const bucketContents = s3.listObjectsV2({
Bucket: config.s3Bucket
}).promise()
.then(result => {
return result.Contents.map(item => item.Key);
})
.catch(err => {
console.error(colors.red.bold('Failed to fetch bucket contents:'), err);
process.exit(1);
});
const uploadDetails = readdir(config.deployFrom)
.then(paths => paths.map(p => {
const key = path.relative(config.deployFrom, p);
return {
Key: key,
Body: fs.createReadStream(p),
...configFor(key)
};
}))
.catch(err => {
console.error(colors.red.bold('Error:'), err);
process.exit(1);
});
Promise.all([bucketContents, uploadDetails]).then(([bucket, upload]) => {
const deleteKeys = bucket.filter(key => !upload.find(conf => key === conf.Key));
const uploadPromises = upload.map(params => {
console.log(`Starting upload for ${ params.Key }`);
return s3.upload({
Bucket: config.s3Bucket,
...params
}).promise()
.then(() => console.log(colors.green(`${ params.Key } successful`)))
.catch(err => {
console.error(colors.red.bold(`${ params.Key } failed`));
return Promise.reject(err);
});
});
return Promise.all(uploadPromises)
.then(() => {
if (deleteKeys.length === 0) {
console.log('No files to delete');
return Promise.resolve();
}
console.log(`Deleting ${ deleteKeys.length } stale files`);
return s3.deleteObjects({
Bucket: config.s3Bucket,
Delete: {
Objects: deleteKeys.map(key => ({ Key: key }))
}
}).promise()
.then(() => console.log(colors.green('Delete successful')))
.catch(err => {
console.error(colors.red.bold('Delete failed'));
return Promise.reject(err);
});
})
.then(() => {
return cloudFront.createInvalidation({
DistributionId: config.cloudFrontId,
InvalidationBatch: {
CallerReference: `deploy-${ process.env.CI_COMMIT_REF_SLUG }-${ process.env.CI_COMMIT_SHA }`,
Paths: {
Quantity: 1,
Items: [
'/*'
]
}
}
}).promise();
});
})
.catch(err => {
console.error(colors.red.bold('Error:'), err);
process.exit(1);
});

View File

@ -1,20 +0,0 @@
import javascript from '../../../src/js/parser/javascript/parser.js';
import _ from 'lodash';
describe('parser/javascript/anchor.js', function() {
_.forIn({
'^': {
label: 'Start of line'
},
'$': {
label: 'End of line'
}
}, (content, str) => {
it(`parses "${str}" as an Anchor`, function() {
var parser = new javascript.Parser(str);
expect(parser.__consume__anchor()).toEqual(jasmine.objectContaining(content));
});
});
});

View File

@ -1,27 +0,0 @@
import javascript from '../../../src/js/parser/javascript/parser.js';
describe('parser/javascript/any_character.js', function() {
it('parses "." as an AnyCharacter', function() {
var parser = new javascript.Parser('.');
expect(parser.__consume__terminal()).toEqual(jasmine.objectContaining({
type: 'any-character'
}));
});
describe('#_render', function() {
beforeEach(function() {
var parser = new javascript.Parser('.');
this.node = parser.__consume__terminal();
});
it('renders a label', function() {
spyOn(this.node, 'renderLabel').and.returnValue('rendered label');
expect(this.node._render()).toEqual('rendered label');
expect(this.node.renderLabel).toHaveBeenCalledWith('any character');
});
});
});

View File

@ -1,32 +0,0 @@
import javascript from '../../../src/js/parser/javascript/parser.js';
import _ from 'lodash';
import Snap from 'snapsvg';
describe('parser/javascript/charset_escape.js', function() {
_.forIn({
'\\b': { label: 'backspace (0x08)', ordinal: 0x08 },
'\\d': { label: 'digit', ordinal: -1 },
'\\D': { label: 'non-digit', ordinal: -1 },
'\\f': { label: 'form feed (0x0C)', ordinal: 0x0c },
'\\n': { label: 'line feed (0x0A)', ordinal: 0x0a },
'\\r': { label: 'carriage return (0x0D)', ordinal: 0x0d },
'\\s': { label: 'white space', ordinal: -1 },
'\\S': { label: 'non-white space', ordinal: -1 },
'\\t': { label: 'tab (0x09)', ordinal: 0x09 },
'\\v': { label: 'vertical tab (0x0B)', ordinal: 0x0b },
'\\w': { label: 'word', ordinal: -1 },
'\\W': { label: 'non-word', ordinal: -1 },
'\\0': { label: 'null (0x00)', ordinal: 0 },
'\\012': { label: 'octal: 12 (0x0A)', ordinal: 10 },
'\\cx': { label: 'ctrl-X (0x18)', ordinal: 24 },
'\\xab': { label: '0xAB', ordinal: 0xab },
'\\uabcd': { label: 'U+ABCD', ordinal: 0xabcd }
}, (content, str) => {
it(`parses "${str}" as a CharsetEscape`, function() {
var parser = new javascript.Parser(str);
expect(parser.__consume__charset_terminal()).toEqual(jasmine.objectContaining(content));
});
});
});

View File

@ -1,103 +0,0 @@
import javascript from '../../../src/js/parser/javascript/parser.js';
import util from '../../../src/js/util.js';
import _ from 'lodash';
describe('parser/javascript/charset_range.js', function() {
_.forIn({
'a-z': {
first: jasmine.objectContaining({ textValue: 'a' }),
last: jasmine.objectContaining({ textValue: 'z' })
},
'\\b-z': {
first: jasmine.objectContaining({ textValue: '\\b' }),
last: jasmine.objectContaining({ textValue: 'z' })
},
'\\f-z': {
first: jasmine.objectContaining({ textValue: '\\f' }),
last: jasmine.objectContaining({ textValue: 'z' })
},
'\\n-z': {
first: jasmine.objectContaining({ textValue: '\\n' }),
last: jasmine.objectContaining({ textValue: 'z' })
},
'\\r-z': {
first: jasmine.objectContaining({ textValue: '\\r' }),
last: jasmine.objectContaining({ textValue: 'z' })
},
'\\t-z': {
first: jasmine.objectContaining({ textValue: '\\t' }),
last: jasmine.objectContaining({ textValue: 'z' })
},
'\\v-z': {
first: jasmine.objectContaining({ textValue: '\\v' }),
last: jasmine.objectContaining({ textValue: 'z' })
}
}, (content, str) => {
it(`parses "${str}" as a CharsetRange`, function() {
var parser = new javascript.Parser(str);
expect(parser.__consume__charset_range()).toEqual(jasmine.objectContaining(content));
});
});
_.each([
'\\d-a',
'\\D-a',
'\\s-a',
'\\S-a',
'\\w-a',
'\\W-a'
], str => {
it(`does not parse "${str}" as a CharsetRange`, function() {
var parser = new javascript.Parser(str);
expect(parser.__consume__charset_range()).toEqual(null);
});
});
it('throws an exception when the range is out of order', function() {
var parser = new javascript.Parser('z-a');
expect(() => {
parser.__consume__charset_range();
}).toThrow('Range out of order in character class: z-a');
});
describe('#_render', function() {
beforeEach(function() {
var parser = new javascript.Parser('a-z');
this.node = parser.__consume__charset_range();
this.node.container = jasmine.createSpyObj('cotnainer', ['addClass', 'text', 'group']);
this.node.container.text.and.returnValue('hyphen');
this.firstDeferred = this.testablePromise();
this.lastDeferred = this.testablePromise();
spyOn(this.node.first, 'render').and.returnValue(this.firstDeferred.promise);
spyOn(this.node.last, 'render').and.returnValue(this.lastDeferred.promise);
spyOn(util, 'spaceHorizontally');
});
it('renders a hyphen', function() {
this.node._render();
expect(this.node.container.text).toHaveBeenCalledWith(0, 0, '-');
});
it('spaces the items horizontally', function(done) {
this.firstDeferred.resolve();
this.lastDeferred.resolve();
this.node._render()
.then(() => {
expect(util.spaceHorizontally).toHaveBeenCalledWith([
this.node.first,
'hyphen',
this.node.last
], { padding: 5 });
done();
});
});
});
});

View File

@ -1,154 +0,0 @@
import javascript from '../../../src/js/parser/javascript/parser.js';
import Node from '../../../src/js/parser/javascript/node.js';
import util from '../../../src/js/util.js';
import _ from 'lodash';
import Snap from 'snapsvg';
describe('parser/javascript/charset.js', function() {
_.forIn({
'[abc]': {
label: 'One of:',
elements: [
jasmine.objectContaining({ type: 'literal', textValue: 'a' }),
jasmine.objectContaining({ type: 'literal', textValue: 'b' }),
jasmine.objectContaining({ type: 'literal', textValue: 'c' })
]
},
'[^abc]': {
label: 'None of:',
elements: [
jasmine.objectContaining({ type: 'literal', textValue: 'a' }),
jasmine.objectContaining({ type: 'literal', textValue: 'b' }),
jasmine.objectContaining({ type: 'literal', textValue: 'c' })
]
},
'[aaa]': {
label: 'One of:',
elements: [
jasmine.objectContaining({ type: 'literal', textValue: 'a' })
]
},
'[a-z]': {
label: 'One of:',
elements: [
jasmine.objectContaining({ type: 'charset-range', textValue: 'a-z' })
]
},
'[\\b]': {
label: 'One of:',
elements: [
jasmine.objectContaining({ type: 'charset-escape', textValue: '\\b' })
]
}
}, (content, str) => {
it(`parses "${str}" as a Charset`, function() {
var parser = new javascript.Parser(str);
expect(parser.__consume__charset()).toEqual(jasmine.objectContaining(content));
});
});
it('adds a warning for character sets the contain non-standard escapes', function() {
var node;
Node.state = { warnings: [] };
node = new javascript.Parser('[\\c]').__consume__charset();
expect(node.state.warnings).toEqual(['The character set "[\\c]" contains the \\c escape followed by a character other than A-Z. This can lead to different behavior depending on browser. The representation here is the most common interpretation.']);
});
describe('_anchor property', function() {
it('calculates the anchor based on the partContainer', function() {
var node = new javascript.Parser('[a]').__consume__charset();
node.partContainer = jasmine.createSpyObj('partContainer', ['getBBox']);
node.partContainer.getBBox.and.returnValue({
cy: 20
});
spyOn(node, 'transform').and.returnValue({
localMatrix: Snap.matrix().translate(3, 8)
});
expect(node._anchor).toEqual({
ay: 28
});
});
});
describe('#_render', function() {
beforeEach(function() {
var counter = 0;
this.node = new javascript.Parser('[a]').__consume__charset();
this.node.label = 'example label';
this.node.elements = [
jasmine.createSpyObj('item', ['render']),
jasmine.createSpyObj('item', ['render']),
jasmine.createSpyObj('item', ['render'])
];
this.elementDeferred = [
this.testablePromise(),
this.testablePromise(),
this.testablePromise()
];
this.node.elements[0].render.and.returnValue(this.elementDeferred[0].promise);
this.node.elements[1].render.and.returnValue(this.elementDeferred[1].promise);
this.node.elements[2].render.and.returnValue(this.elementDeferred[2].promise);
this.node.container = Snap(document.createElement('svg')).group();
this.partContainer = this.node.container.group();
spyOn(this.node.container, 'group').and.returnValue(this.partContainer);
spyOn(this.partContainer, 'group').and.callFake(function() {
return `group ${counter++}`;
});
spyOn(this.node, 'renderLabeledBox').and.returnValue('labeled box promise');
spyOn(util, 'spaceVertically');
});
it('creates a cotainer for the parts of the charset', function() {
this.node._render();
expect(this.node.partContainer).toEqual(this.partContainer);
});
it('renders each item', function() {
this.node._render();
expect(this.node.elements[0].render).toHaveBeenCalledWith('group 0');
expect(this.node.elements[1].render).toHaveBeenCalledWith('group 1');
expect(this.node.elements[2].render).toHaveBeenCalledWith('group 2');
});
describe('positioning of the items', function() {
beforeEach(function() {
this.elementDeferred[0].resolve();
this.elementDeferred[1].resolve();
this.elementDeferred[2].resolve();
});
it('spaces the elements vertically', function(done) {
this.node._render()
.then(() => {
expect(util.spaceVertically).toHaveBeenCalledWith(this.node.elements, { padding: 5 });
done();
});
});
it('renders a labeled box', function(done) {
this.node._render()
.then(result => {
expect(this.node.renderLabeledBox).toHaveBeenCalledWith('example label', this.partContainer, { padding: 5 });
expect(result).toEqual('labeled box promise');
done();
});
});
});
});
});

View File

@ -1,72 +0,0 @@
import javascript from '../../../src/js/parser/javascript/parser.js';
import _ from 'lodash';
import Snap from 'snapsvg';
describe('parser/javascript/escape.js', function() {
_.forIn({
'\\b': { label: 'word boundary', ordinal: -1 },
'\\B': { label: 'non-word boundary', ordinal: -1 },
'\\d': { label: 'digit', ordinal: -1 },
'\\D': { label: 'non-digit', ordinal: -1 },
'\\f': { label: 'form feed (0x0C)', ordinal: 0x0c },
'\\n': { label: 'line feed (0x0A)', ordinal: 0x0a },
'\\r': { label: 'carriage return (0x0D)', ordinal: 0x0d },
'\\s': { label: 'white space', ordinal: -1 },
'\\S': { label: 'non-white space', ordinal: -1 },
'\\t': { label: 'tab (0x09)', ordinal: 0x09 },
'\\v': { label: 'vertical tab (0x0B)', ordinal: 0x0b },
'\\w': { label: 'word', ordinal: -1 },
'\\W': { label: 'non-word', ordinal: -1 },
'\\0': { label: 'null (0x00)', ordinal: 0 },
'\\1': { label: 'Back reference (group = 1)', ordinal: -1 },
'\\2': { label: 'Back reference (group = 2)', ordinal: -1 },
'\\3': { label: 'Back reference (group = 3)', ordinal: -1 },
'\\4': { label: 'Back reference (group = 4)', ordinal: -1 },
'\\5': { label: 'Back reference (group = 5)', ordinal: -1 },
'\\6': { label: 'Back reference (group = 6)', ordinal: -1 },
'\\7': { label: 'Back reference (group = 7)', ordinal: -1 },
'\\8': { label: 'Back reference (group = 8)', ordinal: -1 },
'\\9': { label: 'Back reference (group = 9)', ordinal: -1 },
'\\012': { label: 'octal: 12 (0x0A)', ordinal: 10 },
'\\cx': { label: 'ctrl-X (0x18)', ordinal: 24 },
'\\xab': { label: '0xAB', ordinal: 0xab },
'\\uabcd': { label: 'U+ABCD', ordinal: 0xabcd }
}, (content, str) => {
it(`parses "${str}" as an Escape`, function() {
var parser = new javascript.Parser(str);
expect(parser.__consume__terminal()).toEqual(jasmine.objectContaining(content));
});
});
describe('#_render', function() {
beforeEach(function() {
var parser = new javascript.Parser('\\b');
this.node = parser.__consume__terminal();
this.node.state = {};
this.svg = Snap(document.createElement('svg'));
this.node.container = this.svg.group();
spyOn(this.node, 'renderLabel').and.callThrough();
});
it('renders a label', function() {
this.node._render();
expect(this.node.renderLabel).toHaveBeenCalledWith('word boundary');
});
it('sets the edge radius of the rect', function(done) {
this.node._render()
.then(label => {
expect(label.select('rect').attr()).toEqual(jasmine.objectContaining({
rx: '3',
ry: '3'
}));
done();
});
});
});
});

View File

@ -1,77 +0,0 @@
import javascript from '../../../src/js/parser/javascript/parser.js';
import Snap from 'snapsvg';
describe('parser/javascript/literal.js', function() {
it('parses "x" as a Literal', function() {
var parser = new javascript.Parser('x');
expect(parser.__consume__terminal()).toEqual(jasmine.objectContaining({
type: 'literal',
literal: 'x',
ordinal: 120
}));
});
it('parses "\\x" as a Literal', function() {
var parser = new javascript.Parser('\\x');
expect(parser.__consume__terminal()).toEqual(jasmine.objectContaining({
type: 'literal',
literal: 'x',
ordinal: 120
}));
});
describe('#_render', function() {
beforeEach(function() {
var parser = new javascript.Parser('a');
this.node = parser.__consume__terminal();
this.node.state = {};
this.svg = Snap(document.createElement('svg'));
this.node.container = this.svg.group();
spyOn(this.node, 'renderLabel').and.callThrough();
});
it('renders a label', function() {
this.node._render();
expect(this.node.renderLabel).toHaveBeenCalledWith(['\u201c', 'a', '\u201d']);
});
it('sets the class of the first and third tspan to "quote"', function(done) {
this.node._render()
.then(label => {
expect(label.selectAll('tspan')[0].hasClass('quote')).toBeTruthy();
expect(label.selectAll('tspan')[2].hasClass('quote')).toBeTruthy();
done();
});
});
it('sets the edge radius of the rect', function(done) {
this.node._render()
.then(label => {
expect(label.select('rect').attr()).toEqual(jasmine.objectContaining({
rx: '3',
ry: '3'
}));
done();
});
});
});
describe('#merge', function() {
beforeEach(function() {
var parser = new javascript.Parser('a');
this.node = parser.__consume__terminal();
});
it('appends to the literal value', function() {
this.node.merge({ literal: 'b' });
expect(this.node.literal).toEqual('ab');
});
});
});

View File

@ -1,210 +0,0 @@
import javascript from '../../../src/js/parser/javascript/parser.js';
import _ from 'lodash';
import Snap from 'snapsvg';
describe('parser/javascript/match_fragment.js', function() {
_.forIn({
'a': {
proxy: jasmine.objectContaining({ textValue: 'a' }),
canMerge: true
},
'\\b': {
proxy: jasmine.objectContaining({ textValue: '\\b' }),
canMerge: false
},
'a*': {
content: jasmine.objectContaining({ textValue: 'a' }),
repeat: jasmine.objectContaining({ textValue: '*' }),
canMerge: false
}
}, (content, str) => {
it(`parses "${str}" as a MatchFragment`, function() {
var parser = new javascript.Parser(str);
expect(parser.__consume__match_fragment()).toEqual(jasmine.objectContaining(content));
});
});
describe('_anchor property', function() {
beforeEach(function() {
this.node = new javascript.Parser('a').__consume__match_fragment();
this.node.content = {
getBBox() {
return {
ax: 1,
ax2: 2,
ay: 3
};
}
};
spyOn(this.node, 'transform').and.returnValue({
localMatrix: Snap.matrix().translate(10, 20)
});
});
it('applies the local transform to the content anchor', function() {
expect(this.node._anchor).toEqual({
ax: 11,
ax2: 12,
ay: 23
});
});
});
describe('#_render', function() {
beforeEach(function() {
this.node = new javascript.Parser('a').__consume__match_fragment();
this.node.container = jasmine.createSpyObj('container', [
'addClass',
'group',
'prepend',
'path'
]);
this.node.container.group.and.returnValue('example group');
this.renderDeferred = this.testablePromise();
this.node.content = jasmine.createSpyObj('content', [
'render',
'transform',
'getBBox'
]);
this.node.content.getBBox.and.returnValue('content bbox');
this.node.content.render.and.returnValue(this.renderDeferred.promise);
this.node.repeat = {
contentPosition: 'example position',
skipPath: jasmine.createSpy('skipPath').and.returnValue('skip path'),
loopPath: jasmine.createSpy('loopPath').and.returnValue('loop path')
};
spyOn(this.node, 'loopLabel');
});
it('renders the content', function() {
this.node._render();
expect(this.node.content.render).toHaveBeenCalledWith('example group');
});
describe('positioning of content', function() {
beforeEach(function() {
this.renderDeferred.resolve();
});
it('moves the content to the correct position', function(done) {
this.node._render()
.then(() => {
expect(this.node.content.transform).toHaveBeenCalledWith('example position');
done();
});
});
it('renders a skip path and loop path', function(done) {
this.node._render()
.then(() => {
expect(this.node.repeat.skipPath).toHaveBeenCalledWith('content bbox');
expect(this.node.repeat.loopPath).toHaveBeenCalledWith('content bbox');
expect(this.node.container.path).toHaveBeenCalledWith('skip pathloop path');
done();
});
});
it('renders a loop label', function(done) {
this.node._render()
.then(() => {
expect(this.node.loopLabel).toHaveBeenCalled();
done();
});
});
});
});
describe('#loopLabel', function() {
beforeEach(function() {
this.node = new javascript.Parser('a').__consume__match_fragment();
this.node.repeat = {};
this.node.container = jasmine.createSpyObj('container', [
'addClass',
'text'
]);
this.text = jasmine.createSpyObj('text', [
'addClass',
'getBBox',
'transform'
]);
this.node.container.text.and.returnValue(this.text);
this.text.addClass.and.returnValue(this.text);
this.text.getBBox.and.returnValue({
width: 11,
height: 22
});
spyOn(this.node, 'getBBox').and.returnValue({
x2: 33,
y2: 44
});
});
describe('when a label is defined', function() {
beforeEach(function() {
this.node.repeat.label = 'example label';
});
it('renders a text element', function() {
this.node.loopLabel();
expect(this.node.container.text).toHaveBeenCalledWith(0, 0, ['example label']);
});
describe('when there is a skip loop', function() {
beforeEach(function() {
this.node.repeat.hasSkip = true;
});
it('positions the text element', function() {
this.node.loopLabel();
expect(this.text.transform).toHaveBeenCalledWith(Snap.matrix()
.translate(17, 66));
});
});
describe('when there is no skip loop', function() {
beforeEach(function() {
this.node.repeat.hasSkip = false;
});
it('positions the text element', function() {
this.node.loopLabel();
expect(this.text.transform).toHaveBeenCalledWith(Snap.matrix()
.translate(22, 66));
});
});
});
describe('when a label is not defined', function() {
it('does not render a text element', function() {
this.node.loopLabel();
expect(this.node.container.text).not.toHaveBeenCalled();
});
});
});
});

View File

@ -1,196 +0,0 @@
import javascript from '../../../src/js/parser/javascript/parser.js';
import util from '../../../src/js/util.js';
import _ from 'lodash';
import Snap from 'snapsvg';
describe('parser/javascript/match.js', function() {
_.forIn({
'example': {
parts: [
jasmine.objectContaining({
content: jasmine.objectContaining({ literal: 'example' })
})
],
proxy: jasmine.objectContaining({
content: jasmine.objectContaining({ literal: 'example' })
})
},
'example*': {
parts: [
jasmine.objectContaining({
content: jasmine.objectContaining({ literal: 'exampl' })
}),
jasmine.objectContaining({
content: jasmine.objectContaining({ literal: 'e' })
})
]
},
'': {
parts: []
}
}, (content, str) => {
it(`parses "${str}" as a Match`, function() {
var parser = new javascript.Parser(str);
expect(parser.__consume__match()).toEqual(jasmine.objectContaining(content));
});
});
describe('_anchor property', function() {
beforeEach(function() {
this.node = new javascript.Parser('a').__consume__match();
this.node.start = jasmine.createSpyObj('start', ['getBBox']);
this.node.start.getBBox.and.returnValue({
x: 1,
x2: 2,
cy: 3
});
this.node.end = jasmine.createSpyObj('start', ['getBBox']);
this.node.end.getBBox.and.returnValue({
x: 4,
x2: 5,
cy: 6
});
spyOn(this.node, 'transform').and.returnValue({
localMatrix: Snap.matrix().translate(10, 20)
});
});
it('calculates the anchor from the start and end items', function() {
expect(this.node._anchor).toEqual({
ax: 11,
ax2: 15,
ay: 23
});
});
});
describe('#_render', function() {
beforeEach(function() {
this.node = new javascript.Parser('a').__consume__match();
this.node.container = jasmine.createSpyObj('container', [
'addClass',
'group',
'prepend',
'path'
]);
this.node.container.group.and.returnValue('example group');
this.node.parts = [
jasmine.createSpyObj('part 0', ['render']),
jasmine.createSpyObj('part 1', ['render']),
jasmine.createSpyObj('part 2', ['render'])
];
this.partDeferreds = [
this.testablePromise(),
this.testablePromise(),
this.testablePromise()
];
this.node.parts[0].render.and.returnValue(this.partDeferreds[0].promise);
this.node.parts[1].render.and.returnValue(this.partDeferreds[1].promise);
this.node.parts[2].render.and.returnValue(this.partDeferreds[2].promise);
});
it('renders each part', function() {
this.node._render();
expect(this.node.parts[0].render).toHaveBeenCalledWith('example group');
expect(this.node.parts[1].render).toHaveBeenCalledWith('example group');
expect(this.node.parts[2].render).toHaveBeenCalledWith('example group');
});
describe('positioning of items', function() {
beforeEach(function() {
this.partDeferreds[0].resolve('part 0');
this.partDeferreds[1].resolve('part 1');
this.partDeferreds[2].resolve('part 2');
spyOn(util, 'spaceHorizontally');
spyOn(this.node, 'connectorPaths').and.returnValue(['connector paths']);
});
it('sets the start and end properties', function(done) {
this.node._render()
.then(() => {
expect(this.node.start).toEqual('part 0');
expect(this.node.end).toEqual('part 2');
done();
});
});
it('spaces the items horizontally', function(done) {
this.node._render()
.then(() => {
expect(util.spaceHorizontally).toHaveBeenCalledWith([
'part 0',
'part 1',
'part 2'
], { padding: 10 });
done();
});
});
it('renders the connector paths', function(done) {
this.node._render()
.then(() => {
expect(this.node.connectorPaths).toHaveBeenCalledWith([
'part 0',
'part 1',
'part 2'
]);
expect(this.node.container.path).toHaveBeenCalledWith('connector paths');
done();
});
});
});
});
describe('#connectorPaths', function() {
beforeEach(function() {
this.node = new javascript.Parser('a').__consume__match();
this.items = [
jasmine.createSpyObj('item 0', ['getBBox']),
jasmine.createSpyObj('item 1', ['getBBox']),
jasmine.createSpyObj('item 2', ['getBBox'])
];
this.items[0].getBBox.and.returnValue({
x: 10,
x2: 20,
cy: 5
});
this.items[1].getBBox.and.returnValue({
x: 30,
x2: 40,
cy: 5
});
this.items[2].getBBox.and.returnValue({
x: 50,
x2: 60,
cy: 5
});
});
it('returns the connector paths between fragments', function() {
expect(this.node.connectorPaths(this.items)).toEqual([
'M20,5H30',
'M40,5H50'
]);
});
});
});

View File

@ -1,422 +0,0 @@
import Node from '../../../src/js/parser/javascript/node.js';
import Snap from 'snapsvg';
describe('parser/javascript/node.js', function() {
beforeEach(function() {
Node.state = {};
this.node = new Node();
});
it('references the state from Node.state', function() {
Node.state.example = 'example state';
expect(this.node.state.example).toEqual('example state');
});
describe('module setter', function() {
it('extends the node with the module', function() {
this.node.module = { example: 'value' };
expect(this.node.example).toEqual('value');
});
it('calls the module #setup method', function() {
var setup = jasmine.createSpy('setup');
this.node.module = { setup };
expect(setup).toHaveBeenCalled();
});
it('sets up any defined properties and removes \'definedProperties\' field', function() {
this.node.module = {
definedProperties: {
example: {
get: function() {
return 'value';
}
}
}
};
expect(this.node.example).toEqual('value');
expect(this.node.definedProperties).toBeUndefined();
});
});
describe('container setter', function() {
it('adds a class to the container element', function() {
var container = jasmine.createSpyObj('container', ['addClass']);
this.node.type = 'example type';
this.node.container = container;
expect(container.addClass).toHaveBeenCalledWith('example type');
});
});
describe('anchor getter', function() {
describe('when a proxy node is used', function() {
it('returns the anchor from the proxy', function() {
this.node.proxy = { anchor: 'example anchor' };
expect(this.node.anchor).toEqual('example anchor');
});
});
describe('when a proxy node is not used', function() {
it('returns _anchor of the node', function() {
this.node._anchor = { example: 'value' };
expect(this.node.anchor).toEqual({
example: 'value'
});
});
});
});
describe('#getBBox', function() {
it('returns the normalized bbox of the container merged with the anchor', function() {
this.node.proxy = {
anchor: {
anchor: 'example anchor'
}
};
this.node.container = jasmine.createSpyObj('container', ['addClass', 'getBBox']);
this.node.container.getBBox.and.returnValue({
bbox: 'example bbox',
x: 'left',
x2: 'right',
cy: 'center'
});
expect(this.node.getBBox()).toEqual({
bbox: 'example bbox',
anchor: 'example anchor',
x: 'left',
x2: 'right',
cy: 'center',
ax: 'left',
ax2: 'right',
ay: 'center'
});
});
});
describe('#transform', function() {
it('returns the result of calling transform on the container', function() {
this.node.container = jasmine.createSpyObj('container', ['addClass', 'transform']);
this.node.container.transform.and.returnValue('transform result');
expect(this.node.transform('matrix')).toEqual('transform result');
expect(this.node.container.transform).toHaveBeenCalledWith('matrix');
});
});
describe('#deferredStep', function() {
it('resolves the returned promise when the render is not canceled', function(done) {
var resolve = jasmine.createSpy('resolve'),
reject = jasmine.createSpy('reject');
this.node.deferredStep('result')
.then(resolve, reject)
.then(() => {
expect(resolve).toHaveBeenCalledWith('result');
expect(reject).not.toHaveBeenCalled();
done();
});
});
it('rejects the returned promise when the render is canceled', function(done) {
var resolve = jasmine.createSpy('resolve'),
reject = jasmine.createSpy('reject');
this.node.state.cancelRender = true;
this.node.deferredStep('result', 'value')
.then(resolve, reject)
.then(() => {
expect(resolve).not.toHaveBeenCalled();
expect(reject).toHaveBeenCalledWith('Render cancelled');
done();
});
});
});
describe('#renderLabel', function() {
beforeEach(function() {
this.group = jasmine.createSpyObj('group', ['addClass', 'rect', 'text']);
this.group.addClass.and.returnValue(this.group);
this.node.container = jasmine.createSpyObj('container', ['addClass', 'group']);
this.node.container.group.and.returnValue(this.group);
});
it('adds a "label" class to the group', function() {
this.node.renderLabel('example label');
expect(this.group.addClass).toHaveBeenCalledWith('label');
});
it('creates a rect element', function() {
this.node.renderLabel('example label');
expect(this.group.rect).toHaveBeenCalled();
});
it('creates a text element', function() {
this.node.renderLabel('example label');
expect(this.group.text).toHaveBeenCalledWith(0, 0, ['example label']);
});
describe('positioning of label elements', function() {
beforeEach(function() {
this.text = jasmine.createSpyObj('text', ['getBBox', 'transform']);
this.rect = jasmine.createSpyObj('rect', ['attr']);
this.text.getBBox.and.returnValue({
width: 42,
height: 24
});
this.group.text.and.returnValue(this.text);
this.group.rect.and.returnValue(this.rect);
});
it('transforms the text element', function(done) {
this.node.renderLabel('example label')
.then(() => {
expect(this.text.transform).toHaveBeenCalledWith(Snap.matrix()
.translate(5, 22));
done();
});
});
it('sets the dimensions of the rect element', function(done) {
this.node.renderLabel('example label')
.then(() => {
expect(this.rect.attr).toHaveBeenCalledWith({
width: 52,
height: 34
});
done();
});
});
it('resolves with the group element', function(done) {
this.node.renderLabel('example label')
.then(group => {
expect(group).toEqual(this.group);
done();
});
});
});
});
describe('#render', function() {
beforeEach(function() {
this.container = jasmine.createSpyObj('container', ['addClass']);
});
describe('when a proxy node is used', function() {
beforeEach(function() {
this.node.proxy = jasmine.createSpyObj('proxy', ['render']);
this.node.proxy.render.and.returnValue('example proxy result');
});
it('sets the container', function() {
this.node.render(this.container);
expect(this.node.container).toEqual(this.container);
});
it('calls the proxy render method', function() {
expect(this.node.render(this.container)).toEqual('example proxy result');
expect(this.node.proxy.render).toHaveBeenCalledWith(this.container);
});
});
describe('when a proxy node is not used', function() {
beforeEach(function() {
this.deferred = this.testablePromise();
this.node._render = jasmine.createSpy('_render').and.returnValue(this.deferred.promise);
});
it('sets the container', function() {
this.node.render(this.container);
expect(this.node.container).toEqual(this.container);
});
it('increments the renderCounter', function() {
this.node.state.renderCounter = 0;
this.node.render(this.container);
expect(this.node.state.renderCounter).toEqual(1);
});
it('calls #_render', function() {
this.node.render(this.container);
expect(this.node._render).toHaveBeenCalled();
});
describe('when #_render is complete', function() {
it('decrements the renderCounter', function(done) {
this.node.render(this.container)
.then(() => {
expect(this.node.state.renderCounter).toEqual(41);
done();
});
this.node.state.renderCounter = 42;
this.deferred.resolve();
});
it('ultimately resolves with the node instance', function(done) {
this.deferred.resolve();
this.node.render(this.container)
.then(result => {
expect(result).toEqual(this.node);
done();
});
});
});
});
});
describe('#renderLabeledBox', function() {
beforeEach(function() {
var svg = Snap(document.createElement('svg'));
this.text = svg.text();
this.rect = svg.rect();
this.content = svg.rect();
this.node.container = jasmine.createSpyObj('container', ['addClass', 'text', 'rect', 'prepend']);
this.node.container.text.and.returnValue(this.text);
this.node.container.rect.and.returnValue(this.rect);
this.node.type = 'example-type';
});
it('creates a text element', function() {
this.node.renderLabeledBox('example label', this.content, { padding: 5 });
expect(this.node.container.text).toHaveBeenCalledWith(0, 0, ['example label']);
});
it('sets the class on the text element', function() {
spyOn(this.text, 'addClass').and.callThrough();
this.node.renderLabeledBox('example label', this.content, { padding: 5 });
expect(this.text.addClass).toHaveBeenCalledWith('example-type-label');
});
it('creates a rect element', function() {
this.node.renderLabeledBox('example label', this.content, { padding: 5 });
expect(this.node.container.rect).toHaveBeenCalled();
});
it('sets the class on the rect element', function() {
spyOn(this.rect, 'addClass').and.callThrough();
this.node.renderLabeledBox('example label', this.content, { padding: 5 });
expect(this.rect.addClass).toHaveBeenCalledWith('example-type-box');
});
it('sets the corner radius on the rect element', function() {
spyOn(this.rect, 'attr').and.callThrough();
this.node.renderLabeledBox('example label', this.content, { padding: 5 });
expect(this.rect.attr).toHaveBeenCalledWith({
rx: 3,
ry: 3
});
});
describe('positioning of elements', function() {
beforeEach(function() {
spyOn(this.text, 'getBBox').and.returnValue({
width: 100,
height: 20
});
spyOn(this.content, 'getBBox').and.returnValue({
width: 200,
height: 100,
cx: 100
});
});
it('positions the text element', function(done) {
spyOn(this.text, 'transform').and.callThrough();
this.node.renderLabeledBox('example label', this.content, { padding: 5 })
.then(() => {
expect(this.text.transform).toHaveBeenCalledWith(Snap.matrix()
.translate(0, 20));
done();
});
});
it('positions the rect element', function(done) {
spyOn(this.rect, 'transform').and.callThrough();
this.node.renderLabeledBox('example label', this.content, { padding: 5 })
.then(() => {
expect(this.rect.transform).toHaveBeenCalledWith(Snap.matrix()
.translate(0, 20));
done();
});
});
it('sets the dimensions of the rect element', function(done) {
spyOn(this.rect, 'attr').and.callThrough();
this.node.renderLabeledBox('example label', this.content, { padding: 5 })
.then(() => {
expect(this.rect.attr).toHaveBeenCalledWith({
width: 210,
height: 110
});
done();
});
});
it('sets the dimensions of the rect element (based on the text element)', function(done) {
this.content.getBBox.and.returnValue({
width: 50,
height: 100,
cx: 25
});
spyOn(this.rect, 'attr').and.callThrough();
this.node.renderLabeledBox('example label', this.content, { padding: 5 })
.then(() => {
expect(this.rect.attr).toHaveBeenCalledWith({
width: 100,
height: 110
});
done();
});
});
it('positions the content element', function(done) {
spyOn(this.content, 'transform').and.callThrough();
this.node.renderLabeledBox('example label', this.content, { padding: 5 })
.then(() => {
expect(this.content.transform).toHaveBeenCalledWith(Snap.matrix()
.translate(5, 25));
done();
});
});
});
});
});

View File

@ -1,30 +0,0 @@
import ParserState from '../../../src/js/parser/javascript/parser_state.js';
describe('parser/javascript/parser_state.js', function() {
beforeEach(function() {
this.progress = { style: {} };
this.state = new ParserState(this.progress);
});
describe('renderCounter property', function() {
it('sets the width of the progress element to the percent of completed steps', function() {
this.state.renderCounter = 50;
expect(this.progress.style.width).toEqual('0.00%');
this.state.renderCounter = 10;
expect(this.progress.style.width).toEqual('80.00%');
});
it('does not change the width of the progress element when rendering has been cancelled', function() {
this.state.renderCounter = 50;
this.state.renderCounter = 40;
expect(this.progress.style.width).toEqual('20.00%');
this.state.cancelRender = true;
this.state.renderCounter = 10;
expect(this.progress.style.width).toEqual('20.00%');
});
});
});

View File

@ -1,311 +0,0 @@
import javascript from '../../../src/js/parser/javascript/parser.js';
import util from '../../../src/js/util.js';
import _ from 'lodash';
import Snap from 'snapsvg';
describe('parser/javascript/regexp.js', function() {
_.forIn({
'test': {
proxy: jasmine.objectContaining({ textValue: 'test' })
},
'part 1|part 2': {
matches: [
jasmine.objectContaining({ textValue: 'part 1' }),
jasmine.objectContaining({ textValue: 'part 2' })
]
}
}, (content, str) => {
it(`parses "${str}" as a Regexp`, function() {
var parser = new javascript.Parser(str);
expect(parser.__consume__regexp()).toEqual(jasmine.objectContaining(content));
});
});
describe('#_render', function() {
beforeEach(function() {
var counter = 0;
this.node = new javascript.Parser('a|b').__consume__regexp();
this.node.container = jasmine.createSpyObj('container', [
'addClass',
'group',
'prepend',
'path'
]);
this.group = jasmine.createSpyObj('group', [
'addClass',
'transform',
'group',
'prepend',
'path',
'getBBox'
]);
this.node.container.group.and.returnValue(this.group);
this.group.addClass.and.returnValue(this.group);
this.group.transform.and.returnValue(this.group);
this.group.getBBox.and.returnValue('group bbox');
this.group.group.and.callFake(function() {
return `group ${counter++}`;
});
this.node.matches = [
jasmine.createSpyObj('match', ['render']),
jasmine.createSpyObj('match', ['render']),
jasmine.createSpyObj('match', ['render'])
];
this.matchDeferred = [
this.testablePromise(),
this.testablePromise(),
this.testablePromise()
];
this.node.matches[0].render.and.returnValue(this.matchDeferred[0].promise);
this.node.matches[1].render.and.returnValue(this.matchDeferred[1].promise);
this.node.matches[2].render.and.returnValue(this.matchDeferred[2].promise);
spyOn(this.node, 'getBBox').and.returnValue('container bbox');
spyOn(this.node, 'makeCurve').and.returnValue('curve');
spyOn(this.node, 'makeSide').and.returnValue('side');
spyOn(this.node, 'makeConnector').and.returnValue('connector');
spyOn(util, 'spaceVertically');
});
it('creates a container for the match nodes', function() {
this.node._render();
expect(this.node.container.group).toHaveBeenCalled();
expect(this.group.addClass).toHaveBeenCalledWith('regexp-matches');
expect(this.group.transform).toHaveBeenCalledWith(Snap.matrix()
.translate(20, 0));
});
it('renders each match node', function() {
this.node._render();
expect(this.node.matches[0].render).toHaveBeenCalledWith('group 0');
expect(this.node.matches[1].render).toHaveBeenCalledWith('group 1');
expect(this.node.matches[2].render).toHaveBeenCalledWith('group 2');
});
describe('positioning of the match nodes', function() {
beforeEach(function() {
this.matchDeferred[0].resolve();
this.matchDeferred[1].resolve();
this.matchDeferred[2].resolve();
});
it('spaces the nodes vertically', function(done) {
this.node._render()
.then(() => {
expect(util.spaceVertically).toHaveBeenCalledWith(this.node.matches, { padding: 5 });
done();
});
});
it('renders the sides and curves into the container', function(done) {
this.node._render()
.then(() => {
expect(this.node.makeCurve).toHaveBeenCalledWith('container bbox', this.node.matches[0]);
expect(this.node.makeCurve).toHaveBeenCalledWith('container bbox', this.node.matches[1]);
expect(this.node.makeCurve).toHaveBeenCalledWith('container bbox', this.node.matches[2]);
expect(this.node.makeSide).toHaveBeenCalledWith('container bbox', this.node.matches[0]);
expect(this.node.makeSide).toHaveBeenCalledWith('container bbox', this.node.matches[2]);
expect(this.node.container.path).toHaveBeenCalledWith('curvecurvecurvesideside');
done();
});
});
it('renders the connectors into the match container', function(done) {
this.node._render()
.then(() => {
expect(this.node.makeConnector).toHaveBeenCalledWith('group bbox', this.node.matches[0]);
expect(this.node.makeConnector).toHaveBeenCalledWith('group bbox', this.node.matches[1]);
expect(this.node.makeConnector).toHaveBeenCalledWith('group bbox', this.node.matches[2]);
expect(this.group.path).toHaveBeenCalledWith('connectorconnectorconnector');
done();
});
});
});
});
describe('#madeSide', function() {
beforeEach(function() {
this.node = new javascript.Parser('a|b').__consume__regexp();
this.containerBox = {
cy: 50,
width: 30
};
this.matchBox = {
};
this.match = jasmine.createSpyObj('match', ['getBBox']);
this.match.getBBox.and.returnValue(this.matchBox);
});
describe('when the match node is 15px or more from the centerline', function() {
describe('when the match node is above the centerline', function() {
beforeEach(function() {
this.matchBox.ay = 22;
});
it('returns the vertical sideline to the match node', function() {
expect(this.node.makeSide(this.containerBox, this.match)).toEqual([
'M0,50q10,0 10,-10V32',
'M70,50q-10,0 -10,-10V32'
]);
});
});
describe('when the match node is below the centerline', function() {
beforeEach(function() {
this.matchBox.ay = 88;
});
it('returns the vertical sideline to the match node', function() {
expect(this.node.makeSide(this.containerBox, this.match)).toEqual([
'M0,50q10,0 10,10V78',
'M70,50q-10,0 -10,10V78'
]);
});
});
});
describe('when the match node is less than 15px from the centerline', function() {
beforeEach(function() {
this.matchBox.ay = 44;
});
it('returns nothing', function() {
expect(this.node.makeSide(this.containerBox, this.match)).toBeUndefined();
});
});
});
describe('#makeCurve', function() {
beforeEach(function() {
this.node = new javascript.Parser('a|b').__consume__regexp();
this.containerBox = {
cy: 50,
width: 30
};
this.matchBox = {};
this.match = jasmine.createSpyObj('match', ['getBBox']);
this.match.getBBox.and.returnValue(this.matchBox);
});
describe('when the match node is 15px or more from the centerline', function() {
describe('when the match node is above the centerline', function() {
beforeEach(function() {
this.matchBox.ay = 22;
});
it('returns the curve to the match node', function() {
expect(this.node.makeCurve(this.containerBox, this.match)).toEqual([
'M10,32q0,-10 10,-10',
'M60,32q0,-10 -10,-10'
]);
});
});
describe('when the match node is below the centerline', function() {
beforeEach(function() {
this.matchBox.ay = 88;
});
it('returns the curve to the match node', function() {
expect(this.node.makeCurve(this.containerBox, this.match)).toEqual([
'M10,78q0,10 10,10',
'M60,78q0,10 -10,10'
]);
});
});
});
describe('when the match node is less than 15px from the centerline', function() {
describe('when the match node is above the centerline', function() {
beforeEach(function() {
this.matchBox.ay = 44;
});
it('returns the curve to the match node', function() {
expect(this.node.makeCurve(this.containerBox, this.match)).toEqual([
'M0,50c10,0 10,-6 20,-6',
'M70,50c-10,0 -10,-6 -20,-6'
]);
});
});
describe('when the match node is below the centerline', function() {
beforeEach(function() {
this.matchBox.ay = 55;
});
it('returns the curve to the match node', function() {
expect(this.node.makeCurve(this.containerBox, this.match)).toEqual([
'M0,50c10,0 10,5 20,5',
'M70,50c-10,0 -10,5 -20,5'
]);
});
});
});
});
describe('#makeConnector', function() {
beforeEach(function() {
this.node = new javascript.Parser('a|b').__consume__regexp();
this.containerBox = {
width: 4
};
this.matchBox = {
ay: 1,
ax: 2,
ax2: 3
};
this.match = jasmine.createSpyObj('match', ['getBBox']);
this.match.getBBox.and.returnValue(this.matchBox);
});
it('returns a line from the curve to the match node', function() {
expect(this.node.makeConnector(this.containerBox, this.match)).toEqual('M0,1h2M3,1H4');
});
});
});

View File

@ -1,13 +0,0 @@
import javascript from '../../../src/js/parser/javascript/parser.js';
describe('parser/javascript/repeat_any.js', function() {
it('parses "*" as a RepeatAny', function() {
var parser = new javascript.Parser('*');
expect(parser.__consume__repeat_any()).toEqual(jasmine.objectContaining({
minimum: 0,
maximum: -1
}));
});
});

View File

@ -1,13 +0,0 @@
import javascript from '../../../src/js/parser/javascript/parser.js';
describe('parser/javascript/repeat_optional.js', function() {
it('parses "?" as a RepeatOptional', function() {
var parser = new javascript.Parser('?');
expect(parser.__consume__repeat_optional()).toEqual(jasmine.objectContaining({
minimum: 0,
maximum: 1
}));
});
});

View File

@ -1,13 +0,0 @@
import javascript from '../../../src/js/parser/javascript/parser.js';
describe('parser/javascript/repeat_required.js', function() {
it('parses "+" as a RepeatRequired', function() {
var parser = new javascript.Parser('+');
expect(parser.__consume__repeat_required()).toEqual(jasmine.objectContaining({
minimum: 1,
maximum: -1
}));
});
});

View File

@ -1,410 +0,0 @@
import javascript from '../../../src/js/parser/javascript/parser.js';
import _ from 'lodash';
import Snap from 'snapsvg';
describe('parser/javascript/repeat.js', function() {
_.forIn({
'*': {
minimum: 0,
maximum: -1,
greedy: true,
hasSkip: true,
hasLoop: true
},
'*?': {
minimum: 0,
maximum: -1,
greedy: false,
hasSkip: true,
hasLoop: true
},
'+': {
minimum: 1,
maximum: -1,
greedy: true,
hasSkip: false,
hasLoop: true
},
'+?': {
minimum: 1,
maximum: -1,
greedy: false,
hasSkip: false,
hasLoop: true
},
'?': {
minimum: 0,
maximum: 1,
greedy: true,
hasSkip: true,
hasLoop: false
},
'??': {
minimum: 0,
maximum: 1,
greedy: false,
hasSkip: true,
hasLoop: false
},
'{1}': {
minimum: 1,
maximum: 1,
greedy: true,
hasSkip: false,
hasLoop: false
},
'{0}': {
minimum: 0,
maximum: 0,
greedy: true,
hasSkip: true,
hasLoop: false
},
'{1}?': {
minimum: 1,
maximum: 1,
greedy: false,
hasSkip: false,
hasLoop: false
},
'{2}': {
minimum: 2,
maximum: 2,
greedy: true,
hasSkip: false,
hasLoop: true
},
'{2}?': {
minimum: 2,
maximum: 2,
greedy: false,
hasSkip: false,
hasLoop: true
},
'{0,}': {
minimum: 0,
maximum: -1,
greedy: true,
hasSkip: true,
hasLoop: true
},
'{0,}?': {
minimum: 0,
maximum: -1,
greedy: false,
hasSkip: true,
hasLoop: true
},
'{1,}': {
minimum: 1,
maximum: -1,
greedy: true,
hasSkip: false,
hasLoop: true
},
'{1,}?': {
minimum: 1,
maximum: -1,
greedy: false,
hasSkip: false,
hasLoop: true
},
'{0,1}': {
minimum: 0,
maximum: 1,
greedy: true,
hasSkip: true,
hasLoop: false
},
'{0,1}?': {
minimum: 0,
maximum: 1,
greedy: false,
hasSkip: true,
hasLoop: false
},
'{0,2}': {
minimum: 0,
maximum: 2,
greedy: true,
hasSkip: true,
hasLoop: true
},
'{0,2}?': {
minimum: 0,
maximum: 2,
greedy: false,
hasSkip: true,
hasLoop: true
},
'{1,2}': {
minimum: 1,
maximum: 2,
greedy: true,
hasSkip: false,
hasLoop: true
},
'{1,2}?': {
minimum: 1,
maximum: 2,
greedy: false,
hasSkip: false,
hasLoop: true
}
}, (content, str) => {
it(`parses "${str}" as a Repeat`, function() {
var parser = new javascript.Parser(str);
expect(parser.__consume__repeat()).toEqual(jasmine.objectContaining(content));
});
});
describe('contentPosition property', function() {
beforeEach(function() {
this.node = new javascript.Parser('*').__consume__repeat();
});
_.each([
{
hasLoop: false,
hasSkip: false,
translate: { x: 0, y: 0 }
},
{
hasLoop: true,
hasSkip: false,
translate: { x: 10, y: 0 }
},
{
hasLoop: false,
hasSkip: true,
translate: { x: 15, y: 10 }
},
{
hasLoop: true,
hasSkip: true,
translate: { x: 15, y: 10 }
}
], t => {
it(`translates to [${t.translate.x}, ${t.translate.y}] when hasLoop is ${t.hasLoop} and hasSkip is ${t.hasSkip}`, function() {
this.node.hasLoop = t.hasLoop;
this.node.hasSkip = t.hasSkip;
expect(this.node.contentPosition).toEqual(Snap.matrix()
.translate(t.translate.x, t.translate.y));
});
});
});
describe('label property', function() {
beforeEach(function() {
this.node = new javascript.Parser('*').__consume__repeat();
});
_.each([
{
minimum: 1,
maximum: -1,
label: undefined
},
{
minimum: 0,
maximum: 0,
label: undefined
},
{
minimum: 2,
maximum: -1,
label: '1+ times'
},
{
minimum: 3,
maximum: -1,
label: '2+ times'
},
{
minimum: 0,
maximum: 2,
label: 'at most once'
},
{
minimum: 0,
maximum: 3,
label: 'at most 2 times'
},
{
minimum: 2,
maximum: 2,
label: 'once'
},
{
minimum: 3,
maximum: 3,
label: '2 times'
},
{
minimum: 2,
maximum: 3,
label: '1\u20262 times'
},
{
minimum: 3,
maximum: 4,
label: '2\u20263 times'
}
], t => {
it(`is "${t.label}" when minimum=${t.minimum} and maximum=${t.maximum}`, function() {
this.node.minimum = t.minimum;
this.node.maximum = t.maximum;
expect(this.node.label).toEqual(t.label);
});
});
});
describe('tooltip property', function() {
beforeEach(function() {
this.node = new javascript.Parser('*').__consume__repeat();
});
_.each([
{
minimum: 1,
maximum: -1,
tooltip: undefined
},
{
minimum: 0,
maximum: 0,
tooltip: undefined
},
{
minimum: 2,
maximum: -1,
tooltip: 'repeats 2+ times in total'
},
{
minimum: 3,
maximum: -1,
tooltip: 'repeats 3+ times in total'
},
{
minimum: 0,
maximum: 2,
tooltip: 'repeats at most 2 times in total'
},
{
minimum: 0,
maximum: 3,
tooltip: 'repeats at most 3 times in total'
},
{
minimum: 2,
maximum: 2,
tooltip: 'repeats 2 times in total'
},
{
minimum: 3,
maximum: 3,
tooltip: 'repeats 3 times in total'
},
{
minimum: 2,
maximum: 3,
tooltip: 'repeats 2\u20263 times in total'
},
{
minimum: 3,
maximum: 4,
tooltip: 'repeats 3\u20264 times in total'
}
], t => {
it(`is "${t.tooltip}" when minimum=${t.minimum} and maximum=${t.maximum}`, function() {
this.node.minimum = t.minimum;
this.node.maximum = t.maximum;
expect(this.node.tooltip).toEqual(t.tooltip);
});
});
});
describe('#skipPath', function() {
beforeEach(function() {
this.node = new javascript.Parser('*').__consume__repeat();
this.box = {
y: 11,
ay: 22,
width: 33
};
});
it('returns nothing when there is no skip', function() {
this.node.hasSkip = false;
expect(this.node.skipPath(this.box)).toEqual([]);
});
it('returns a path when there is a skip', function() {
this.node.hasSkip = true;
this.node.greedy = true;
expect(this.node.skipPath(this.box)).toEqual([
'M0,22q10,0 10,-10v-1q0,-10 10,-10h23q10,0 10,10v1q0,10 10,10'
]);
});
it('returns a path with arrow when there is a non-greedy skip', function() {
this.node.hasSkip = true;
this.node.greedy = false;
expect(this.node.skipPath(this.box)).toEqual([
'M0,22q10,0 10,-10v-1q0,-10 10,-10h23q10,0 10,10v1q0,10 10,10',
'M10,7l5,5m-5,-5l-5,5'
]);
});
});
describe('#loopPath', function() {
beforeEach(function() {
this.node = new javascript.Parser('*').__consume__repeat();
this.box = {
x: 11,
x2: 22,
ay: 33,
y2: 44,
width: 55
};
});
it('returns nothing when there is no loop', function() {
this.node.hasLoop = false;
expect(this.node.loopPath(this.box)).toEqual([]);
});
it('returns a path when there is a loop', function() {
this.node.hasLoop = true;
this.node.greedy = false;
expect(this.node.loopPath(this.box)).toEqual([
'M11,33q-10,0 -10,10v1q0,10 10,10h55q10,0 10,-10v-1q0,-10 -10,-10'
]);
});
it('returns a path with arrow when there is a greedy loop', function() {
this.node.hasLoop = true;
this.node.greedy = true;
expect(this.node.loopPath(this.box)).toEqual([
'M11,33q-10,0 -10,10v1q0,10 10,10h55q10,0 10,-10v-1q0,-10 -10,-10',
'M32,48l5,-5m-5,5l-5,-5'
]);
});
});
});

View File

@ -1,41 +0,0 @@
import javascript from '../../../src/js/parser/javascript/parser.js';
describe('parser/javascript/repeat_spec.js', function() {
it('parses "{n,m}" as a RepeatSpec (with minimum and maximum values)', function() {
var parser = new javascript.Parser('{24,42}');
expect(parser.__consume__repeat_spec()).toEqual(jasmine.objectContaining({
minimum: 24,
maximum: 42
}));
});
it('parses "{n,}" as a RepeatSpec (with only minimum value)', function() {
var parser = new javascript.Parser('{24,}');
expect(parser.__consume__repeat_spec()).toEqual(jasmine.objectContaining({
minimum: 24,
maximum: -1
}));
});
it('parses "{n}" as a RepeatSpec (with an exact count)', function() {
var parser = new javascript.Parser('{24}');
expect(parser.__consume__repeat_spec()).toEqual(jasmine.objectContaining({
minimum: 24,
maximum: 24
}));
});
it('does not parse "{,m}" as a RepeatSpec', function() {
var parser = new javascript.Parser('{,42}');
expect(parser.__consume__repeat_spec()).toEqual(null);
});
it('throws an exception when the numbers are out of order', function() {
var parser = new javascript.Parser('{42,24}');
expect(() => {
parser.__consume__repeat_spec();
}).toThrow('Numbers out of order: {42,24}');
});
});

View File

@ -1,175 +0,0 @@
import javascript from '../../../src/js/parser/javascript/parser.js';
import Snap from 'snapsvg';
import _ from 'lodash';
describe('parser/javascript/root.js', function() {
_.forIn({
'test': {
flags: [],
regexp: jasmine.objectContaining({ textValue: 'test' })
},
'/test/': {
flags: [],
regexp: jasmine.objectContaining({ textValue: 'test' })
},
'/test/i': {
flags: ['Ignore Case'],
regexp: jasmine.objectContaining({ textValue: 'test' })
},
'/test/g': {
flags: ['Global'],
regexp: jasmine.objectContaining({ textValue: 'test' })
},
'/test/m': {
flags: ['Multiline'],
regexp: jasmine.objectContaining({ textValue: 'test' })
},
'/test/y': {
flags: ['Sticky'],
regexp: jasmine.objectContaining({ textValue: 'test' })
},
'/test/u': {
flags: ['Unicode'],
regexp: jasmine.objectContaining({ textValue: 'test' })
},
'/test/mgi': {
flags: ['Global', 'Ignore Case', 'Multiline'],
regexp: jasmine.objectContaining({ textValue: 'test' })
}
}, (content, str) => {
it(`parses "${str}" as a Root`, function() {
var parser = new javascript.Parser(str);
expect(parser.__consume__root()).toEqual(jasmine.objectContaining(content));
});
});
describe('#_render', function() {
beforeEach(function() {
this.textElement = jasmine.createSpyObj('text', ['getBBox']);
this.textElement.getBBox.and.returnValue({
height: 20
});
this.node = new javascript.Parser('test').__consume__root();
this.node.container = jasmine.createSpyObj('container', [
'addClass',
'text',
'group',
'path',
'circle'
]);
this.node.container.text.and.returnValue(this.textElement);
this.node.container.group.and.returnValue('group element');
this.node.regexp = jasmine.createSpyObj('regexp', [
'render',
'transform',
'getBBox'
]);
this.renderDeferred = this.testablePromise();
this.node.regexp.render.and.returnValue(this.renderDeferred.promise);
});
it('renders the regexp', function() {
this.node._render();
expect(this.node.regexp.render).toHaveBeenCalledWith('group element');
});
describe('when there are flags', function() {
beforeEach(function() {
this.node.flags = ['example', 'flags'];
});
it('renders a text element', function() {
this.node._render();
expect(this.node.container.text).toHaveBeenCalledWith(0, 0, 'Flags: example, flags');
});
});
describe('when there are no flags', function() {
beforeEach(function() {
this.node.flags = [];
});
it('does not render a text element', function() {
this.node._render();
expect(this.node.container.text).not.toHaveBeenCalled();
});
});
describe('positioning of elements', function() {
beforeEach(function() {
this.renderDeferred.resolve();
this.node.regexp.getBBox.and.returnValue({
ax: 1,
ay: 2,
ax2: 3,
x2: 4
});
});
it('renders a path element to lead in and out of the regexp', function(done) {
this.node._render()
.then(() => {
expect(this.node.container.path).toHaveBeenCalledWith('M1,2H0M3,2H14');
done();
});
});
it('renders circle elements before and after the regexp', function(done) {
this.node._render()
.then(() => {
expect(this.node.container.circle).toHaveBeenCalledWith(0, 2, 5);
expect(this.node.container.circle).toHaveBeenCalledWith(14, 2, 5);
done();
});
});
describe('when there are flags', function() {
beforeEach(function() {
this.node.flags = ['example'];
});
it('moves the regexp below the flag text', function(done) {
this.node._render()
.then(() => {
expect(this.node.regexp.transform).toHaveBeenCalledWith(Snap.matrix()
.translate(10, 20));
done();
});
});
});
describe('when there are no flags', function() {
beforeEach(function() {
this.node.flags = [];
});
it('positions the regexp', function(done) {
this.node._render()
.then(() => {
expect(this.node.regexp.transform).toHaveBeenCalledWith(Snap.matrix()
.translate(10, 0));
done();
});
});
});
});
});
});

View File

@ -1,120 +0,0 @@
import javascript from '../../../src/js/parser/javascript/parser.js';
import Node from '../../../src/js/parser/javascript/node.js';
import _ from 'lodash';
import Snap from 'snapsvg';
describe('parser/javascript/subexp.js', function() {
beforeEach(function() {
Node.state = { groupCounter: 1 };
});
_.forIn({
'(test)': {
regexp: jasmine.objectContaining({ textValue: 'test' })
},
'(?=test)': {
regexp: jasmine.objectContaining({ textValue: 'test' })
},
'(?!test)': {
regexp: jasmine.objectContaining({ textValue: 'test' })
},
'(?:test)': {
regexp: jasmine.objectContaining({ textValue: 'test' }),
proxy: jasmine.objectContaining({ textValue: 'test' })
}
}, (content, str) => {
it(`parses "${str}" as a Subexp`, function() {
var parser = new javascript.Parser(str);
expect(parser.__consume__subexp()).toEqual(jasmine.objectContaining(content));
});
});
describe('_anchor property', function() {
it('applies the local transform matrix to the anchor from the regexp', function() {
var node = new javascript.Parser('(test)').__consume__subexp();
node.regexp = {
getBBox() {
return {
ax: 10,
ax2: 15,
ay: 20
};
}
};
spyOn(node, 'transform').and.returnValue({
localMatrix: Snap.matrix().translate(3, 8)
});
expect(node._anchor).toEqual({
ax: 13,
ax2: 18,
ay: 28
});
});
});
describe('#_render', function() {
beforeEach(function() {
this.renderDeferred = this.testablePromise();
this.node = new javascript.Parser('(test)').__consume__subexp();
this.node.regexp = jasmine.createSpyObj('regexp', ['render']);
this.node.container = jasmine.createSpyObj('container', ['addClass', 'group']);
spyOn(this.node, 'label').and.returnValue('example label')
this.node.regexp.render.and.returnValue(this.renderDeferred.promise);
});
it('renders the regexp', function() {
this.node._render();
expect(this.node.regexp.render).toHaveBeenCalled();
});
it('renders a labeled box', function(done) {
spyOn(this.node, 'renderLabeledBox');
this.renderDeferred.resolve();
this.node._render()
.then(() => {
expect(this.node.renderLabeledBox).toHaveBeenCalledWith('example label', this.node.regexp, { padding: 10 });
done();
});
});
});
describe('#label', function() {
_.forIn({
'(test)': {
label: 'group #1',
groupCounter: 2
},
'(?=test)': {
label: 'positive lookahead',
groupCounter: 1
},
'(?!test)': {
label: 'negative lookahead',
groupCounter: 1
},
'(?:test)': {
label: '',
groupCounter: 1
}
}, (data, str) => {
it(`generates the correct label for "${str}"`, function() {
var node = new javascript.Parser(str).__consume__subexp();
expect(node.label()).toEqual(data.label);
expect(node.state.groupCounter).toEqual(data.groupCounter);
});
});
});
});

View File

@ -1,173 +0,0 @@
import Parser from '../../src/js/parser/javascript.js';
import regexpParser from '../../src/js/parser/javascript/grammar.peg';
import Snap from 'snapsvg';
describe('parser/javascript.js', function() {
beforeEach(function() {
this.container = document.createElement('div');
this.parser = new Parser(this.container);
});
describe('container property', function() {
it('sets the content of the element', function() {
var element = document.createElement('div');
this.parser.container = element;
expect(element.innerHTML).not.toEqual('');
});
it('keeps the original content if the keepContent option is set', function() {
var element = document.createElement('div');
element.innerHTML = 'example content';
this.parser.options.keepContent = true;
this.parser.container = element;
expect(element.innerHTML).toContain('example content');
expect(element.innerHTML).not.toEqual('example content');
});
it('adds the "svg-container" class', function() {
spyOn(this.parser, '_addClass');
this.parser.container = document.createElement('div');
expect(this.parser._addClass).toHaveBeenCalledWith('svg-container');
});
});
describe('#parse', function() {
beforeEach(function() {
spyOn(regexpParser, 'parse');
});
it('adds the "loading" class', function() {
spyOn(this.parser, '_addClass');
this.parser.parse('example expression');
expect(this.parser._addClass).toHaveBeenCalledWith('loading');
});
it('parses the expression', function(done) {
this.parser.parse('example expression')
.then(() => {
expect(regexpParser.parse).toHaveBeenCalledWith('example expression');
done();
});
});
it('replaces newlines with "\\n"', function(done) {
this.parser.parse('multiline\nexpression')
.then(() => {
expect(regexpParser.parse).toHaveBeenCalledWith('multiline\\nexpression');
done();
});
});
it('resolves the returned promise with the parser instance', function(done) {
this.parser.parse('example expression')
.then(result => {
expect(result).toEqual(this.parser);
done();
});
});
it('rejects the returned promise with the exception thrown', function(done) {
regexpParser.parse.and.throwError('fail');
this.parser.parse('(example')
.then(null, result => {
expect(result).toBeDefined();
done();
});
});
});
describe('#render', function() {
beforeEach(function() {
this.renderPromise = this.testablePromise();
this.parser.parsed = jasmine.createSpyObj('parsed', ['render']);
this.parser.parsed.render.and.returnValue(this.renderPromise.promise);
});
it('render the parsed expression', function() {
this.parser.render();
expect(this.parser.parsed.render).toHaveBeenCalled();
});
describe('when rendering is complete', function() {
beforeEach(function() {
this.result = jasmine.createSpyObj('result', ['getBBox', 'transform']);
this.result.getBBox.and.returnValue({
x: 4,
y: 2,
width: 42,
height: 24
});
this.renderPromise.resolve(this.result);
});
it('positions the renderd expression', function(done) {
this.parser.render()
.then(() => {
expect(this.result.transform).toHaveBeenCalledWith(Snap.matrix()
.translate(6, 8));
done();
});
});
it('sets the dimensions of the image', function(done) {
this.parser.render()
.then(() => {
let svg = this.container.querySelector('svg');
expect(svg.getAttribute('width')).toEqual('62');
expect(svg.getAttribute('height')).toEqual('44');
done();
});
});
it('removes the "loading" class', function(done) {
spyOn(this.parser, '_removeClass');
this.parser.render()
.then(() => {
expect(this.parser._removeClass).toHaveBeenCalledWith('loading');
done();
});
});
it('removes the progress element', function(done) {
this.parser.render()
.then(() => {
expect(this.container.querySelector('.loading')).toBeNull();
done();
});
});
});
});
describe('#cancel', function() {
it('sets the cancelRender state to true', function() {
this.parser.cancel();
expect(this.parser.state.cancelRender).toEqual(true);
});
});
describe('warnings property', function() {
it('returns the content of the warnings state variable', function() {
this.parser.state.warnings.push('example');
expect(this.parser.warnings).toEqual(['example']);
});
});
});

View File

@ -1,569 +0,0 @@
import util from '../src/js/util.js';
import Regexper from '../src/js/regexper.js';
import Parser from '../src/js/parser/javascript.js';
import Snap from 'snapsvg';
describe('regexper.js', function() {
beforeEach(function() {
this.root = document.createElement('div');
this.root.innerHTML = [
'<form id="regexp-form" action="/">',
'<input type="text" id="regexp-input">',
'<ul class="example">',
'<ul><a href="#" data-action="permalink"></a></ul>',
'<ul><a href="#" data-action="download-svg"></a></ul>',
'<ul><a href="#" data-action="download-png"></a></ul>',
'</ul>',
'</form>',
'<div id="error"></div>',
'<ul id="warnings"></ul>',
'<div id="regexp-render"></div>'
].join('');
this.regexper = new Regexper(this.root);
spyOn(this.regexper, '_setHash');
spyOn(this.regexper, '_getHash');
});
describe('#keypressListener', function() {
beforeEach(function() {
this.event = util.customEvent('keypress');
spyOn(this.event, 'preventDefault');
spyOn(this.regexper.form, 'dispatchEvent');
});
describe('when the shift key is not depressed', function() {
beforeEach(function() {
this.event.shiftKey = false;
this.event.keyCode = 13;
});
it('does not prevent the default action', function() {
this.regexper.keypressListener(this.event);
expect(this.event.returnValue).not.toEqual(false);
expect(this.event.preventDefault).not.toHaveBeenCalled();
});
it('does not trigger a submit event', function() {
this.regexper.keypressListener(this.event);
expect(this.regexper.form.dispatchEvent).not.toHaveBeenCalled();
});
});
describe('when the keyCode is not 13 (Enter)', function() {
beforeEach(function() {
this.event.shiftKey = true;
this.event.keyCode = 42;
});
it('does not prevent the default action', function() {
this.regexper.keypressListener(this.event);
expect(this.event.returnValue).not.toEqual(false);
expect(this.event.preventDefault).not.toHaveBeenCalled();
});
it('does not trigger a submit event', function() {
this.regexper.keypressListener(this.event);
expect(this.regexper.form.dispatchEvent).not.toHaveBeenCalled();
});
});
describe('when the shift key is depressed and the keyCode is 13 (Enter)', function() {
beforeEach(function() {
this.event.shiftKey = true;
this.event.keyCode = 13;
});
it('prevents the default action', function() {
this.regexper.keypressListener(this.event);
expect(this.event.returnValue).not.toEqual(true);
expect(this.event.preventDefault).toHaveBeenCalled();
});
it('triggers a submit event', function() {
var event;
this.regexper.keypressListener(this.event);
expect(this.regexper.form.dispatchEvent).toHaveBeenCalled();
event = this.regexper.form.dispatchEvent.calls.mostRecent().args[0];
expect(event.type).toEqual('submit');
});
});
});
describe('#documentKeypressListener', function() {
beforeEach(function() {
this.event = util.customEvent('keyup');
this.regexper.running = jasmine.createSpyObj('parser', ['cancel']);
});
describe('when the keyCode is not 27 (Escape)', function() {
beforeEach(function() {
this.event.keyCode = 42;
});
it('does not cancel the parser', function() {
this.regexper.documentKeypressListener(this.event);
expect(this.regexper.running.cancel).not.toHaveBeenCalled();
});
});
describe('when the keyCode is 27 (Escape)', function() {
beforeEach(function() {
this.event.keyCode = 27;
});
it('cancels the parser', function() {
this.regexper.documentKeypressListener(this.event);
expect(this.regexper.running.cancel).toHaveBeenCalled();
});
});
});
describe('#submitListener', function() {
beforeEach(function() {
this.event = util.customEvent('submit');
spyOn(this.event, 'preventDefault');
this.regexper.field.value = 'example value';
});
it('prevents the default action', function() {
this.regexper.submitListener(this.event);
expect(this.event.returnValue).not.toEqual(true);
expect(this.event.preventDefault).toHaveBeenCalled();
});
it('sets the location.hash', function() {
this.regexper.submitListener(this.event);
expect(this.regexper._setHash).toHaveBeenCalledWith('example value');
});
describe('when setting location.hash fails', function() {
beforeEach(function() {
this.regexper._setHash.and.throwError('hash failure');
});
it('disables the permalink', function() {
this.regexper.submitListener(this.event);
expect(this.regexper.permalinkEnabled).toEqual(false);
});
it('shows the expression directly', function() {
spyOn(this.regexper, 'showExpression');
this.regexper.submitListener(this.event);
expect(this.regexper.showExpression).toHaveBeenCalledWith('example value');
});
});
});
describe('#hashchangeListener', function() {
describe('when the URL is invalid', function() {
beforeEach(function() {
this.regexper._getHash.and.returnValue(new Error('example error'));
});
it('displays an error message', function() {
this.regexper.hashchangeListener();
expect(this.regexper.state).toEqual('has-error');
expect(this.regexper.error.innerHTML).toEqual('Malformed expression in URL');
});
it('tracks the event', function() {
this.regexper.hashchangeListener();
expect(util.track).toHaveBeenCalledWith('send', 'event', 'visualization', 'malformed URL');
});
});
describe('when the URL is valid', function() {
beforeEach(function() {
this.regexper._getHash.and.returnValue('example hash value');
});
it('enables the permalink', function() {
this.regexper.hashchangeListener();
expect(this.regexper.permalinkEnabled).toEqual(true);
});
it('shows the expression from the hash', function() {
spyOn(this.regexper, 'showExpression');
this.regexper.hashchangeListener();
expect(this.regexper.showExpression).toHaveBeenCalledWith('example hash value');
});
});
});
describe('#bindListeners', function() {
beforeEach(function() {
spyOn(this.regexper, 'keypressListener');
spyOn(this.regexper, 'submitListener');
spyOn(this.regexper, 'documentKeypressListener');
spyOn(this.regexper, 'hashchangeListener');
});
it('binds #keypressListener to keypress on the text field', function() {
spyOn(this.regexper.field, 'addEventListener');
this.regexper.bindListeners();
expect(this.regexper.field.addEventListener).toHaveBeenCalledWith('keypress', jasmine.any(Function));
this.regexper.field.addEventListener.calls.mostRecent().args[1]();
expect(this.regexper.keypressListener).toHaveBeenCalled();
});
it('binds #submitListener to submit on the form', function() {
spyOn(this.regexper.form, 'addEventListener');
this.regexper.bindListeners();
expect(this.regexper.form.addEventListener).toHaveBeenCalledWith('submit', jasmine.any(Function));
this.regexper.form.addEventListener.calls.mostRecent().args[1]();
expect(this.regexper.submitListener).toHaveBeenCalled();
});
it('binds #documentKeypressListener to keyup on the root', function() {
spyOn(this.regexper.root, 'addEventListener');
this.regexper.bindListeners();
expect(this.regexper.root.addEventListener).toHaveBeenCalledWith('keyup', jasmine.any(Function));
this.regexper.root.addEventListener.calls.mostRecent().args[1]();
expect(this.regexper.documentKeypressListener).toHaveBeenCalled();
});
it('binds #hashchangeListener to hashchange on the window', function() {
spyOn(window, 'addEventListener');
this.regexper.bindListeners();
expect(window.addEventListener).toHaveBeenCalledWith('hashchange', jasmine.any(Function));
window.addEventListener.calls.mostRecent().args[1]();
expect(this.regexper.hashchangeListener).toHaveBeenCalled();
});
});
describe('#showExpression', function() {
beforeEach(function() {
spyOn(this.regexper, 'renderRegexp').and.returnValue(jasmine.createSpyObj('renderRegexp', ['catch']));
});
it('sets the text field value', function() {
this.regexper.showExpression('example expression');
expect(this.regexper.field.value).toEqual('example expression');
});
it('clears the state', function() {
this.regexper.showExpression('');
expect(this.regexper.state).toEqual('');
});
describe('when the expression is not blank', function() {
it('renders the expression', function() {
this.regexper.showExpression('example expression');
expect(this.regexper.renderRegexp).toHaveBeenCalledWith('example expression');
});
});
});
describe('#updateLinks', function() {
beforeEach(function() {
spyOn(this.regexper, 'buildBlobURL');
this.regexper.svgContainer.innerHTML = '<div class="svg">example image</div>';
});
it('builds the blob URL from the SVG image', function() {
this.regexper.updateLinks();
expect(this.regexper.buildBlobURL).toHaveBeenCalledWith('example image');
});
describe('when blob URLs are supported', function() {
beforeEach(function() {
this.regexper.buildBlobURL.and.returnValue('http://example.com/blob');
});
it('sets the download link href', function() {
this.regexper.updateLinks();
expect(this.regexper.downloadSvg.href).toEqual('http://example.com/blob');
});
});
describe('when blob URLs are not supported', function() {
beforeEach(function() {
this.regexper.buildBlobURL.and.throwError('blob failure');
});
it('hides the download link', function() {
this.regexper.updateLinks();
expect(this.regexper.links.className).toMatch(/\bexample\b/);
expect(this.regexper.links.className).toMatch(/\bhide-download\b/);
});
});
describe('when the permalink is enabled', function() {
beforeEach(function() {
this.regexper.permalinkEnabled = true;
});
it('sets the permalink href', function() {
this.regexper.updateLinks();
expect(this.regexper.permalink.href).toEqual(location.toString());
});
});
describe('when the permalink is disabled', function() {
beforeEach(function() {
this.regexper.permalinkEnabled = false;
});
it('hides the permalink', function() {
this.regexper.updateLinks();
expect(this.regexper.links.className).toMatch(/\bexample\b/);
expect(this.regexper.links.className).toMatch(/\bhide-permalink\b/);
});
});
});
describe('#displayWarnings', function() {
it('adds a list item for each warning', function() {
spyOn(util, 'icon').and.returnValue('(icon-markup)');
this.regexper.displayWarnings(['warning 1', 'warning 2']);
expect(this.regexper.warnings.innerHTML).toEqual('<li class="inline-icon">(icon-markup)warning 1</li><li class="inline-icon">(icon-markup)warning 2</li>');
});
});
describe('#renderRegexp', function() {
beforeEach(function() {
this.parsePromise = this.testablePromise();
this.renderPromise = this.testablePromise();
spyOn(Parser.prototype, 'parse').and.returnValue(this.parsePromise.promise);
spyOn(Parser.prototype, 'render').and.returnValue(this.renderPromise.promise);
spyOn(Parser.prototype, 'cancel');
spyOn(this.regexper, 'updateLinks');
spyOn(this.regexper, 'displayWarnings');
});
it('sets the state to "is-loading"', function() {
this.regexper.renderRegexp('example expression');
expect(this.regexper.state).toEqual('is-loading');
});
it('tracks the beginning of the render', function() {
this.regexper.renderRegexp('example expression');
expect(util.track).toHaveBeenCalledWith('send', 'event', 'visualization', 'start');
});
it('keeps a copy of the running property parser', function() {
this.regexper.renderRegexp('example expression');
expect(this.regexper.running).toBeTruthy();
});
it('parses the expression', function() {
this.regexper.renderRegexp('example expression');
expect(this.regexper.running.parse).toHaveBeenCalledWith('example expression');
});
describe('when parsing fails', function() {
beforeEach(function() {
this.parsePromise.reject(new Error('example parse error'));
});
it('sets the state to be "has-error"', function(done) {
this.regexper.renderRegexp('example expression')
.then(() => {
expect(this.regexper.state).toEqual('has-error');
done();
});
});
it('displays the error message', function(done) {
this.regexper.renderRegexp('example expression')
.then(() => {
expect(this.regexper.error.innerHTML).toEqual('Error: example parse error');
done();
});
});
it('tracks the parse error', function(done) {
this.regexper.renderRegexp('example expression')
.then(() => {
expect(util.track).toHaveBeenCalledWith('send', 'event', 'visualization', 'parse error');
done();
});
});
});
describe('when parsing succeeds', function() {
beforeEach(function() {
this.parser = new Parser(this.regexper.svgContainer);
this.parsePromise.resolve(this.parser);
this.renderPromise.resolve();
});
it('renders the expression', function(done) {
this.regexper.renderRegexp('example expression')
.then(() => {
expect(this.parser.render).toHaveBeenCalled();
done();
});
});
});
describe('when rendering is complete', function() {
beforeEach(function() {
this.parser = new Parser(this.regexper.svgContainer);
this.parsePromise.resolve(this.parser);
this.renderPromise.resolve();
});
it('sets the state to "has-results"', function(done) {
this.regexper.renderRegexp('example expression')
.then(() => {
expect(this.regexper.state).toEqual('has-results');
done();
});
});
it('updates the links', function(done) {
this.regexper.renderRegexp('example expression')
.then(() => {
expect(this.regexper.updateLinks).toHaveBeenCalled();
done();
});
});
it('displays the warnings', function(done) {
this.regexper.renderRegexp('example expression')
.then(() => {
expect(this.regexper.displayWarnings).toHaveBeenCalled();
done();
});
});
it('tracks the complete render', function(done) {
this.regexper.renderRegexp('example expression')
.then(() => {
expect(util.track).toHaveBeenCalledWith('send', 'event', 'visualization', 'complete');
done();
});
});
it('sets the running property to false', function(done) {
this.regexper.renderRegexp('example expression')
.then(() => {
expect(this.regexper.running).toBeFalsy();
done();
});
});
it('tracks the total rendering time', function(done) {
this.regexper.renderRegexp('example expression')
.then(() => {
expect(util.track).toHaveBeenCalledWith('send', 'timing', 'visualization', 'total time', jasmine.any(Number));
done();
});
});
});
describe('when the rendering is cancelled', function() {
beforeEach(function() {
this.parser = new Parser(this.regexper.svgContainer);
this.parsePromise.resolve(this.parser);
this.renderPromise.reject('Render cancelled');
});
it('clears the state', function(done) {
this.regexper.renderRegexp('example expression')
.then(() => {
expect(this.regexper.state).toEqual('');
done();
});
});
it('tracks the cancelled render', function(done) {
this.regexper.renderRegexp('example expression')
.then(() => {
expect(util.track).toHaveBeenCalledWith('send', 'event', 'visualization', 'cancelled');
done();
});
});
it('sets the running property to false', function(done) {
this.regexper.renderRegexp('example expression')
.then(() => {
expect(this.regexper.running).toBeFalsy();
done();
});
});
});
describe('when the rendering fails', function() {
beforeEach(function() {
this.parser = new Parser(this.regexper.svgContainer);
this.parsePromise.resolve(this.parser);
this.renderPromise.reject('example render failure');
});
it('sets the running property to false', function(done) {
this.regexper.renderRegexp('example expression')
.then(fail, () => {
expect(this.regexper.running).toBeFalsy();
done();
});
});
});
});
});

View File

@ -1,33 +0,0 @@
import util from '../src/js/util.js';
// Setup (and teardown) SVG container template
beforeEach(function() {
var template = document.createElement('script');
template.setAttribute('type', 'text/html');
template.setAttribute('id', 'svg-container-base');
template.innerHTML = [
'<div class="svg"><svg></svg></div>',
'<div class="progress"><div></div></div>'
].join('');
document.body.appendChild(template);
this.testablePromise = function() {
var result = {};
result.promise = new Promise((resolve, reject) => {
result.resolve = resolve;
result.reject = reject;
});
return result;
};
});
afterEach(function() {
document.body.removeChild(document.body.querySelector('#svg-container-base'));
});
// Spy on util.track to prevent unnecessary logging
beforeEach(function() {
spyOn(util, 'track');
});

View File

@ -1,2 +0,0 @@
var testsContext = require.context(".", true, /_spec$/);
testsContext.keys().forEach(testsContext);

View File

@ -1,97 +0,0 @@
import util from '../src/js/util.js';
describe('util.js', function() {
describe('customEvent', function() {
it('sets the event type', function() {
var event = util.customEvent('example');
expect(event.type).toEqual('example');
});
it('sets the event detail', function() {
var event = util.customEvent('example', 'detail');
expect(event.detail).toEqual('detail');
});
});
describe('normalizeBBox', function() {
it('defaults the anchor keys to values from the bbox', function() {
expect(util.normalizeBBox({
x: 'bbox x',
x2: 'bbox x2',
cy: 'bbox cy',
ay: 'bbox ay'
})).toEqual({
x: 'bbox x',
x2: 'bbox x2',
cy: 'bbox cy',
ax: 'bbox x',
ax2: 'bbox x2',
ay: 'bbox ay'
});
});
});
describe('spaceHorizontally', function() {
it('positions each item', function() {
var svg = Snap(document.createElement('svg')),
items = [
svg.group(),
svg.group(),
svg.group()
];
spyOn(items[0], 'getBBox').and.returnValue({ ay: 5, width: 10 });
spyOn(items[1], 'getBBox').and.returnValue({ ay: 15, width: 30 });
spyOn(items[2], 'getBBox').and.returnValue({ ay: 10, width: 20 });
spyOn(items[0], 'transform').and.callThrough();
spyOn(items[1], 'transform').and.callThrough();
spyOn(items[2], 'transform').and.callThrough();
util.spaceHorizontally(items, { padding: 5 });
expect(items[0].transform).toHaveBeenCalledWith(Snap.matrix()
.translate(0, 10));
expect(items[1].transform).toHaveBeenCalledWith(Snap.matrix()
.translate(15, 0));
expect(items[2].transform).toHaveBeenCalledWith(Snap.matrix()
.translate(50, 5));
});
});
describe('spaceVertically', function() {
it('positions each item', function() {
var svg = Snap(document.createElement('svg')),
items = [
svg.group(),
svg.group(),
svg.group()
];
spyOn(items[0], 'getBBox').and.returnValue({ cx: 5, height: 10 });
spyOn(items[1], 'getBBox').and.returnValue({ cx: 15, height: 30 });
spyOn(items[2], 'getBBox').and.returnValue({ cx: 10, height: 20 });
spyOn(items[0], 'transform').and.callThrough();
spyOn(items[1], 'transform').and.callThrough();
spyOn(items[2], 'transform').and.callThrough();
util.spaceVertically(items, { padding: 5 });
expect(items[0].transform).toHaveBeenCalledWith(Snap.matrix()
.translate(10, 0));
expect(items[1].transform).toHaveBeenCalledWith(Snap.matrix()
.translate(0, 15));
expect(items[2].transform).toHaveBeenCalledWith(Snap.matrix()
.translate(5, 50));
});
});
});

View File

@ -1,17 +0,0 @@
---
title: Page Not Found
---
{{#extend "layout"}}
{{#content "body"}}
<div class="error copy">
<h1>404: Not Found</h1>
<blockquote>
Some people, when confronted with a problem, think<br/>
&ldquo;I know, I'll use regular expressions.&rdquo; Now they have two problems.
</blockquote>
<p>Apparently, you have three problems&hellip;because the page you requested cannot be found.</p>
</div>
{{/content}}
{{/extend}}

View File

@ -0,0 +1,18 @@
/* eslint-disable react/prop-types */
import React from 'react';
import reflowable from 'components/SVG/reflowable';
@reflowable
class SVGElement extends React.PureComponent {
reflow() {
return this.setBBox(this.props.bbox);
}
render() {
return <text>Mock content</text>;
}
}
export default SVGElement;

20
src/__mocks__/i18n.js Normal file
View File

@ -0,0 +1,20 @@
import React from 'react';
import i18n from 'i18next';
import { I18nextProvider } from 'react-i18next';
const translate = txt => `translate(${ txt })`;
i18n.init({
fallbackLng: 'en',
fallbackNS: 'missing',
debug: false,
resources: {}
});
const I18nWrapper = ({ children }) => ( // eslint-disable-line react/prop-types
<I18nextProvider i18n={ i18n }>
{ React.cloneElement(React.Children.only(children), { t: translate }) }
</I18nextProvider>
);
export { translate, i18n, I18nWrapper };

5
src/__mocks__/svgMock.js Normal file
View File

@ -0,0 +1,5 @@
import React from 'react';
const SvgMock = () => <svg></svg>;
export default SvgMock;

View File

@ -1,17 +0,0 @@
---
title: Changelog
---
{{#extend "layout"}}
{{#content "body"}}
<div class="copy changelog">
<dl>
{{#each changelog}}
<dt>{{label}}</dt>
{{#each changes}}
<dd>{{{this}}}</dd>
{{/each}}
{{/each}}
</dl>
</div>
{{/content}}
{{/extend}}

View File

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App rendering 1`] = `
<React.Fragment>
<Translate(Form)
downloadUrls={Array []}
key="expr=undefined&syntax=undefined"
onSubmit={[Function]}
syntaxes={
Object {
"js": "JavaScript",
"pcre": "PCRE",
}
}
/>
</React.Fragment>
`;

231
src/components/App/index.js Normal file
View File

@ -0,0 +1,231 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import URLSearchParams from 'url-search-params';
import Raven from 'raven-js';
import LoaderIcon from 'feather-icons/dist/icons/loader.svg';
import style from './style.css';
import Form from 'components/Form';
import Message from 'components/Message';
import InstallPrompt from 'components/InstallPrompt';
import { demoImage } from 'devel';
const syntaxes = {
js: 'JavaScript',
pcre: 'PCRE'
};
const toUrl = params => new URLSearchParams(params).toString();
class App extends React.PureComponent {
state = {}
image = React.createRef()
componentDidMount() {
window.addEventListener('hashchange', this.handleHashChange);
window.addEventListener('beforeinstallprompt', this.handleInstallPrompt);
this.handleHashChange();
}
componentWillUnmount() {
window.removeEventListener('hashchange', this.handleHashChange);
window.removeEventListener('beforeinstallprompt', this.handleInstallPrompt);
}
async setSvgUrl() {
try {
const type = 'image/svg+xml';
const blob = await this.image.current.svgUrl(type);
this.setState({
svgUrl: {
url: URL.createObjectURL(blob),
label: 'Download SVG',
filename: 'image.svg',
type
}
});
}
catch (e) {
console.error(e); // eslint-disable-line no-console
}
}
async setPngUrl() {
try {
const type = 'image/png';
const blob = await this.image.current.pngUrl(type);
this.setState({
pngUrl: {
url: URL.createObjectURL(blob),
label: 'Download PNG',
filename: 'image.png',
type
}
});
}
catch (e) {
console.error(e); // eslint-disable-line no-console
}
}
async loadSVGComponent() {
if (this.state.SVG) {
return;
}
this.setState({
loading: true,
loadingFailed: false
});
try {
const SVG = await import(/* webpackChunkName: "render" */ 'components/SVG');
this.setState({
SVG: SVG.default,
loading: false
});
}
catch (e) {
Raven.captureException(e);
this.setState({
loading: false,
loadingFailed: e
});
throw e;
}
}
handleInstallPrompt = event => {
event.preventDefault();
this.setState({
installPrompt: event
});
}
handleSubmit = ({expr, syntax}) => {
if (expr) {
document.location.hash = toUrl({ syntax, expr });
}
}
handleHashChange = async () => {
const query = document.location.hash.slice(1);
const params = new URLSearchParams(query);
const { expr, syntax } = (() => {
if (params.get('syntax')) {
return {
syntax: params.get('syntax'),
expr: params.get('expr')
};
} else {
// Assuming old-style URL
return {
syntax: 'js',
expr: query
};
}
})();
if (!expr) {
return;
}
try {
await this.loadSVGComponent();
console.log(syntax, expr); // eslint-disable-line no-console
this.setState({
image: demoImage,
permalinkUrl: document.location.toString(),
syntax,
expr
}, async () => {
await this.image.current.doReflow();
this.setSvgUrl();
this.setPngUrl();
});
}
catch (e) {
console.error(e); // eslint-disable-line no-console
}
}
handleRetry = async event => {
event.preventDefault();
this.handleHashChange();
}
handleInstallReject = () => {
this.setState({ installPrompt: null });
}
handleInstallAccept = async () => {
const { installPrompt } = this.state;
this.setState({ installPrompt: null });
installPrompt.prompt();
}
render() {
const {
SVG,
loading,
loadingFailed,
svgUrl,
pngUrl,
permalinkUrl,
syntax,
expr,
image,
installPrompt
} = this.state;
const downloadUrls = [
svgUrl,
pngUrl
].filter(Boolean);
return <React.Fragment>
<Form
key={ toUrl({ expr, syntax }) }
syntaxes={ syntaxes }
downloadUrls={ downloadUrls }
permalinkUrl={ permalinkUrl }
syntax={ syntax }
expr={ expr }
onSubmit={ this.handleSubmit }/>
{
loading && <div className={ style.loader }>
<LoaderIcon />
<div className={ style.message }>Loading...</div>
</div>
}
{
loadingFailed && <Message type="error" heading="Render Failure">
An error occurred while rendering the regular expression. <a href="#retry" onClick={ this.handleRetry }>Retry</a>
</Message>
}
{
image && <div className={ style.render }>
<SVG data={ image } ref={ this.image }/>
</div>
}
{
installPrompt && <InstallPrompt onAccept={ this.handleInstallAccept } onReject={ this.handleInstallReject } />
}
</React.Fragment>;
}
}
App.propTypes = {
t: PropTypes.func
};
export default translate()(App);
export { App };

View File

@ -0,0 +1,55 @@
@import url('../../globals.css');
.render {
width: 100%;
background: var(--color-white);
box-sizing: border-box;
overflow: auto;
margin: var(--spacing-margin) 0;
& svg {
display: block;
transform: scaleZ(1); /* Move to separate render layer in Chrome */
margin: 0 auto;
}
}
.loader {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 2rem;
background: var(--color-white);
color: var(--color-black);
& .message {
font-weight: bold;
font-size: 2.5rem;
padding: 0;
text-align: center;
}
& svg {
display: block;
transform: scaleZ(1); /* Move to separate render layer in Chrome */
width: 5rem;
height: 5rem;
stroke: var(--color-black);
animation: loader-spin 1s steps(8) infinite;
& line:nth-of-type(1) { stroke: color(var(--color-black) alpha(0.75)); }
& line:nth-of-type(3) { stroke: color(var(--color-black) alpha(0.50)); }
& line:nth-of-type(5) { stroke: color(var(--color-black) alpha(0.25)); }
& line:nth-of-type(7) { stroke: color(var(--color-black) alpha(0)); }
}
}
@keyframes loader-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,16 @@
jest.mock('components/SVG');
import React from 'react';
import { shallow } from 'enzyme';
import { App } from 'components/App';
import { translate } from '__mocks__/i18n';
describe('App', () => {
test('rendering', () => {
const component = shallow(
<App t={ translate }/>
);
expect(component).toMatchSnapshot();
});
});

View File

@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Footer rendering 1`] = `
<footer>
<ul>
<li>
<Trans>
Created by
<a
href="mailto:jeff.avallone@gmail.com"
>
Jeff Avallone
</a>
</Trans>
</li>
<li>
<Trans
i18nKey="Generated images licensed"
>
Generated images licensed:
<a
href="http://creativecommons.org/licenses/by/3.0/"
rel="license external noopener noreferrer"
target="_blank"
>
<img
alt="Creative Commons CC-BY-3.0 License"
src="https://licensebuttons.net/l/by/3.0/80x15.png"
/>
</a>
</Trans>
</li>
</ul>
<div
className="buildId"
>
example build id
</div>
</footer>
`;

View File

@ -0,0 +1,27 @@
import React from 'react';
import { translate, Trans } from 'react-i18next';
import style from './style.css';
const Footer = () => (
<footer>
<ul>
<li>
<Trans>Created by <a href="mailto:jeff.avallone@gmail.com">Jeff Avallone</a></Trans>
</li>
<li>
<Trans i18nKey="Generated images licensed">
Generated images licensed: <a rel="license external noopener noreferrer" target="_blank" href="http://creativecommons.org/licenses/by/3.0/">
<img
alt="Creative Commons CC-BY-3.0 License"
src="https://licensebuttons.net/l/by/3.0/80x15.png" />
</a>
</Trans>
</li>
</ul>
<div className={ style.buildId }>{ process.env.BUILD_ID }</div>
</footer>
);
export default translate()(Footer);
export { Footer };

View File

@ -0,0 +1,27 @@
@import url('../../globals.css');
footer {
display: flex;
align-items: flex-start;
margin: var(--spacing-margin) 0;
@media screen and (max-width: 800px) {
display: block;
}
& ul {
@apply --inline-list;
@apply --with-separator-left;
flex: 1;
}
& img {
vertical-align: text-top;
width: 80px;
height: 15px;
}
& .buildId {
color: color(var(--color-brown) blend(var(--color-tan) 25%));
}
}

View File

@ -0,0 +1,17 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Footer } from 'components/Footer';
describe('Footer', () => {
beforeEach(() => {
process.env.BUILD_ID = 'example build id';
});
test('rendering', () => {
const component = shallow(
<Footer/>
);
expect(component).toMatchSnapshot();
});
});

View File

@ -0,0 +1,194 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Form rendering 1`] = `
<div
className="form"
>
<form
onSubmit={[Function]}
>
<textarea
autoFocus={true}
name="expr"
onChange={[Function]}
onKeyPress={[Function]}
placeholder="translate(Enter regular expression to display)"
/>
<button
type="submit"
>
<Trans>
Display
</Trans>
</button>
<div
className="select"
>
<select
name="syntax"
onChange={[Function]}
value="js"
>
<option
key="js"
value="js"
>
Javascript
</option>
<option
key="pcre"
value="pcre"
>
PCRE
</option>
</select>
<SvgMock />
</div>
<ul
className="actions"
/>
</form>
</div>
`;
exports[`Form rendering with download URLs 1`] = `
<div
className="form"
>
<form
onSubmit={[Function]}
>
<textarea
autoFocus={true}
name="expr"
onChange={[Function]}
onKeyPress={[Function]}
placeholder="translate(Enter regular expression to display)"
/>
<button
type="submit"
>
<Trans>
Display
</Trans>
</button>
<div
className="select"
>
<select
name="syntax"
onChange={[Function]}
value="js"
>
<option
key="js"
value="js"
>
Javascript
</option>
<option
key="pcre"
value="pcre"
>
PCRE
</option>
</select>
<SvgMock />
</div>
<ul
className="actions"
>
<li
key="0"
>
<a
download="image.svg"
href="#svg"
type="image/svg+xml"
>
<SvgMock />
<Trans>
Download SVG
</Trans>
</a>
</li>
<li
key="1"
>
<a
download="image.png"
href="#png"
type="image/png"
>
<SvgMock />
<Trans>
Download PNG
</Trans>
</a>
</li>
</ul>
</form>
</div>
`;
exports[`Form rendering with permalink URL 1`] = `
<div
className="form"
>
<form
onSubmit={[Function]}
>
<textarea
autoFocus={true}
name="expr"
onChange={[Function]}
onKeyPress={[Function]}
placeholder="translate(Enter regular expression to display)"
/>
<button
type="submit"
>
<Trans>
Display
</Trans>
</button>
<div
className="select"
>
<select
name="syntax"
onChange={[Function]}
value="js"
>
<option
key="js"
value="js"
>
Javascript
</option>
<option
key="pcre"
value="pcre"
>
PCRE
</option>
</select>
<SvgMock />
</div>
<ul
className="actions"
>
<li>
<a
href="#permalink"
>
<SvgMock />
<Trans>
Permalink
</Trans>
</a>
</li>
</ul>
</form>
</div>
`;

View File

@ -0,0 +1,105 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate, Trans } from 'react-i18next';
import DownloadIcon from 'feather-icons/dist/icons/download.svg';
import LinkIcon from 'feather-icons/dist/icons/link.svg';
import ExpandIcon from 'feather-icons/dist/icons/chevrons-down.svg';
import style from './style.css';
class Form extends React.PureComponent {
state = {
expr: this.props.expr,
syntax: this.props.syntax || Object.keys(this.props.syntaxes)[0]
}
handleSubmit = event => {
event.preventDefault();
this.props.onSubmit.call(this, {
expr: this.state.expr,
syntax: this.state.syntax
});
}
handleKeyPress = event => {
if (event.charCode === 13 && event.shiftKey) {
this.handleSubmit(event);
}
}
handleChange = event => this.setState({ [event.target.name]: event.target.value })
permalinkAction() {
const { permalinkUrl } = this.props;
if (!permalinkUrl) {
return;
}
return <li>
<a href={ permalinkUrl }><LinkIcon/><Trans>Permalink</Trans></a>
</li>;
}
downloadActions() {
const { downloadUrls } = this.props;
if (!downloadUrls) {
return;
}
return downloadUrls.map(({ url, filename, type, label }, i) => <li key={ i }>
<a href={ url } download={ filename } type={ type }>
<DownloadIcon/><Trans>{ label }</Trans>
</a>
</li>);
}
render() {
const { syntaxes, t } = this.props;
const { expr, syntax } = this.state;
return <div className={ style.form }>
<form onSubmit={ this.handleSubmit }>
<textarea
name="expr"
value={ expr }
onKeyPress={ this.handleKeyPress }
onChange={ this.handleChange }
autoFocus
placeholder={ t('Enter regular expression to display') }></textarea>
<button type="submit"><Trans>Display</Trans></button>
<div className={ style.select }>
<select
name="syntax"
value={ syntax }
onChange={ this.handleChange }>
{ Object.keys(syntaxes).map(id => (
<option value={ id } key={ id }>{ syntaxes[id] }</option>
)) }
</select>
<ExpandIcon/>
</div>
<ul className={ style.actions }>
{ this.downloadActions() }
{ this.permalinkAction() }
</ul>
</form>
</div>;
}
}
Form.propTypes = {
expr: PropTypes.string,
syntax: PropTypes.string,
syntaxes: PropTypes.object,
onSubmit: PropTypes.func,
permalinkUrl: PropTypes.string,
downloadUrls: PropTypes.array,
t: PropTypes.func
};
export default translate()(Form);
export { Form };

View File

@ -0,0 +1,67 @@
@import url('../../globals.css');
:root {
--control-gradient: var(--color-green) var(--gradient-green);
--select-height: 2.8rem;
--select-width: 12rem;
--entry-line-height: 1.5em;
}
.form {
margin: var(--spacing-margin) 0;
overflow: hidden; /* Keep floated content in the box */
& textarea {
display: block;
font-size: inherit;
line-height: var(--entry-line-height);
border: 0 none;
outline: none;
background: var(--color-tan);
padding: 0 1rem;
margin-bottom: var(--spacing-margin);
width: 100% !important; /* "!important" to prevent user changing width */
height: calc(3 * var(--entry-line-height));
box-sizing: border-box;
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
}
& textarea::placeholder {
color: var(--color-brown);
}
& button {
font-size: inherit;
font-weight: bold;
line-height: 2.8rem;
width: 10rem;
border: 0 none;
background: var(--control-gradient);
color: var(--color-black);
cursor: pointer;
padding: 0;
margin-right: 1rem;
}
}
.actions {
@apply --list-reset;
margin-top: var(--spacing-margin);
@media (min-width: 700px) {
@apply --inline-list;
@apply --with-separator-left;
float: right;
}
& svg {
width: 1em;
height: 1em;
margin-right: 0.5rem;
vertical-align: middle;
}
}
.select {
@apply --fancy-select;
}

View File

@ -0,0 +1,93 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Form } from 'components/Form';
import { translate } from '__mocks__/i18n';
const syntaxes = {
js: 'Javascript',
pcre: 'PCRE'
};
describe('Form', () => {
test('rendering', () => {
const component = shallow(
<Form t={ translate } syntaxes={ syntaxes }/>
);
expect(component).toMatchSnapshot();
});
test('rendering with download URLs', () => {
const downloadUrls = [
{ url: '#svg', filename: 'image.svg', type: 'image/svg+xml', label: 'Download SVG' },
{ url: '#png', filename: 'image.png', type: 'image/png', label: 'Download PNG' }
];
const component = shallow(
<Form t={ translate } syntaxes={ syntaxes } downloadUrls={ downloadUrls }/>
);
expect(component).toMatchSnapshot();
});
test('rendering with permalink URL', () => {
const permalinkUrl = '#permalink';
const component = shallow(
<Form t={ translate } syntaxes={ syntaxes } permalinkUrl={ permalinkUrl }/>
);
expect(component).toMatchSnapshot();
});
describe('submitting expression', () => {
test('submitting form', () => {
const onSubmit = jest.fn();
const component = shallow(
<Form t={ translate } syntaxes={ syntaxes } onSubmit={ onSubmit }/>
);
const exprInput = component.find('[name="expr"]');
const syntaxInput = component.find('[name="syntax"]');
exprInput.simulate('change', { target: { name: 'expr', value: 'Test expression' } });
syntaxInput.simulate('change', { target: { name: 'syntax', value: 'test' } });
const eventObj = { preventDefault: jest.fn() };
component.find('form').simulate('submit', eventObj);
expect(eventObj.preventDefault).toHaveBeenCalled();
expect(onSubmit).toHaveBeenCalledWith({
expr: 'Test expression',
syntax: 'test'
});
});
test('submitting form with Shift+Enter', () => {
const component = shallow(
<Form t={ translate } syntaxes={ syntaxes } onSubmit={ Function.prototype }/>
);
const form = component.instance();
const eventObj = {
preventDefault: Function.prototype,
charCode: 13,
shiftKey: true
};
jest.spyOn(form, 'handleSubmit');
component.find('textarea').simulate('keypress', eventObj);
expect(form.handleSubmit).toHaveBeenCalled();
});
test('not submitting with just Enter', () => {
const component = shallow(
<Form t={ translate } syntaxes={ syntaxes } onSubmit={ Function.protoytpe }/>
);
const form = component.instance();
const eventObj = {
preventDefault: Function.prototype,
charCode: 13,
shiftKey: false
};
jest.spyOn(form, 'handleSubmit');
component.find('textarea').simulate('keypress', eventObj);
expect(form.handleSubmit).not.toHaveBeenCalled();
});
});
});

View File

@ -0,0 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header rendering 1`] = `
<header
className="header"
data-banner="testing"
>
<h1>
<a
href="/"
>
Regexper
</a>
</h1>
<ul>
<li>
<a
href="https://gitlab.com/javallone/regexper-static"
rel="external noopener noreferrer"
target="_blank"
>
<SvgMock />
<Trans>
Source on GitLab
</Trans>
</a>
</li>
<li>
<a
href="/privacy.html"
>
<Trans>
Privacy Policy
</Trans>
</a>
</li>
<li>
<Translate(LocaleSwitcher) />
</li>
</ul>
</header>
`;

View File

@ -0,0 +1,28 @@
import React from 'react';
import { translate, Trans } from 'react-i18next';
import style from './style.css';
import GitlabIcon from 'feather-icons/dist/icons/gitlab.svg';
import LocaleSwitcher from 'components/LocaleSwitcher';
const Header = () => (
<header className={ style.header } data-banner={ process.env.BANNER }>
<h1>
<a href="/">Regexper</a>
</h1>
<ul>
<li><a href="https://gitlab.com/javallone/regexper-static" rel="external noopener noreferrer" target="_blank">
<GitlabIcon/><Trans>Source on GitLab</Trans>
</a></li>
<li>
<a href="/privacy.html"><Trans>Privacy Policy</Trans></a>
</li>
<li><LocaleSwitcher /></li>
</ul>
</header>
);
export default translate()(Header);
export { Header };

View File

@ -0,0 +1,68 @@
@import url('../../globals.css');
.header {
display: flex;
align-items: center;
background: var(--color-green) var(--gradient-green);
box-shadow: 0 0 1rem color(var(--color-black) alpha(0.7));
padding: 0 var(--content-margin);
margin: 0 calc(-1 * var(--content-margin)) var(--spacing-margin) calc(-1 * var(--content-margin));
position: relative;
color: var(--color-black);
&:after {
content: attr(data-banner);
pointer-events: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: var(--header-height);
line-height: var(--header-height);
text-transform: uppercase;
text-align: center;
font-size: calc(var(--header-height) * 0.7);
font-weight: bold;
opacity: 0.2;
}
& h1 {
flex-grow: 1;
font-family: 'Bangers', 'cursive';
font-size: 4rem;
font-weight: normal;
display: inline-block;
margin: 0;
line-height: var(--header-height);
text-shadow: 0 0 5px var(--color-green);
}
& a {
text-decoration: none;
display: inline-block;
}
& ul {
@apply --inline-list;
@apply --with-separator-right;
text-align: right;
margin: 1rem 0;
}
& li {
line-height: 2.4rem;
& a:hover,
& a:active {
text-decoration: underline;
}
& a svg {
display: inline-block;
width: 1em;
height: 1em;
margin-right: 0.5rem;
vertical-align: text-top;
}
}
}

View File

@ -0,0 +1,17 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Header } from 'components/Header';
describe('Header', () => {
beforeEach(() => {
process.env.BANNER = 'testing';
});
test('rendering', () => {
const component = shallow(
<Header/>
);
expect(component).toMatchSnapshot();
});
});

View File

@ -0,0 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`InstallPrompt rendering 1`] = `
<div
className="install"
>
<p
className="cta"
>
<Trans>
Add Regexper to your home screen?
</Trans>
</p>
<div
className="actions"
>
<button
className="primary"
>
<Trans>
Add It
</Trans>
</button>
<button>
<Trans>
No Thanks
</Trans>
</button>
</div>
</div>
`;

View File

@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate, Trans } from 'react-i18next';
import style from './style.css';
const InstallPrompt = ({ onAccept, onReject }) => (
<div className={ style.install }>
<p className={ style.cta }><Trans>Add Regexper to your home screen?</Trans></p>
<div className={ style.actions }>
<button className={ style.primary } onClick={ onAccept }><Trans>Add It</Trans></button>
<button onClick={ onReject }><Trans>No Thanks</Trans></button>
</div>
</div>
);
InstallPrompt.propTypes = {
onAccept: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired
};
export default translate()(InstallPrompt);
export { InstallPrompt };

View File

@ -0,0 +1,38 @@
@import url('../../globals.css');
.install {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--color-tan);
color: var(--color-black);
}
.cta {
margin: 0;
padding: var(--spacing-margin);
text-align: center;
}
.actions {
display: flex;
flex-wrap: nowrap;
justify-content: space-evenly;
padding: var(--spacing-margin);
& button {
font-size: inherit;
line-height: 2.8rem;
border: 0 none;
background: var(--color-green) var(--gradient-green);
color: var(--color-black);
cursor: pointer;
padding: 0;
width: 40vw;
&.primary {
font-weight: bold;
}
}
}

View File

@ -0,0 +1,14 @@
import React from 'react';
import { shallow } from 'enzyme';
import { InstallPrompt } from 'components/InstallPrompt';
import { translate } from '__mocks__/i18n';
describe('InstallPrompt', () => {
test('rendering', () => {
const component = shallow(
<InstallPrompt t={ translate }/>
);
expect(component).toMatchSnapshot();
});
});

View File

@ -0,0 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LocaleSwitcher rendering 1`] = `
<label>
<Trans>
Language
</Trans>
<div
className="switcher"
>
<select
onChange={[Function]}
value="en"
>
<option
key="en"
value="en"
>
/displayName
</option>
<option
key="fr"
value="fr"
>
/displayName
</option>
</select>
<SvgMock />
</div>
</label>
`;

View File

@ -0,0 +1,63 @@
import React from 'react';
import { translate, Trans } from 'react-i18next';
import i18n from 'i18next';
import style from './style.css';
import ExpandIcon from 'feather-icons/dist/icons/chevrons-down.svg';
import locales from 'locales';
const localeToAvailable = (locale, available, defaultLocale) => {
if (available.includes(locale)) {
return locale;
}
const parts = locale.split('-');
if (parts.length > 0 && available.includes(parts[0])) {
return parts[0];
}
return defaultLocale;
};
class LocaleSwitcher extends React.PureComponent {
state = {
current: localeToAvailable(i18n.language || '', Object.keys(locales), 'en')
}
componentDidMount() {
i18n.on('languageChanged', this.handleLanguageChange);
}
componentWillUnmount() {
i18n.off('languageChanged', this.handleLanguageChange);
}
handleSelectChange = ({ target }) => {
i18n.changeLanguage(target.value);
}
handleLanguageChange = lang => {
this.setState({ current: lang });
}
render() {
const { current } = this.state;
return <label>
<Trans>Language</Trans>
<div className={ style.switcher }>
<select value={ current } onChange={ this.handleSelectChange }>
{ Object.keys(locales).map(locale => (
<option value={ locale } key={ locale }>{ i18n.getFixedT(locale)('/displayName') }</option>
)) }
</select>
<ExpandIcon/>
</div>
</label>;
}
}
export default translate()(LocaleSwitcher);
export { LocaleSwitcher };

View File

@ -0,0 +1,11 @@
@import url('../../globals.css');
:root {
--control-gradient: var(--color-tan) var(--gradient-tan);
--select-height: 2.4rem;
--select-width: 10rem;
}
.switcher {
@apply --fancy-select;
}

View File

@ -0,0 +1,31 @@
jest.mock('components/SVG');
jest.mock('locales', () => ({
en: {},
fr: {}
}));
import React from 'react';
import { shallow } from 'enzyme';
import { LocaleSwitcher } from 'components/LocaleSwitcher';
import { translate } from '__mocks__/i18n';
describe('LocaleSwitcher', () => {
test('rendering', () => {
const component = shallow(
<LocaleSwitcher t={ translate }/>
);
expect(component).toMatchSnapshot();
});
test('changing language', () => {
const component = shallow(
<LocaleSwitcher t={ translate }/>
);
const selectInput = component.find('select');
selectInput.value = 'fr';
selectInput.simulate('change', { target: { value: 'fr' } });
expect(component.state('current')).toEqual('fr');
});
});

View File

@ -0,0 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Message rendering 1`] = `
<div
className="message"
>
<div
className="header"
>
<h2>
Testing
</h2>
</div>
<div
className="content"
>
<p>
Message content
</p>
</div>
</div>
`;
exports[`Message rendering with icon 1`] = `
<div
class="message"
>
<div
class="header"
>
Sample icon SVG
<h2>
Testing
</h2>
</div>
<div
class="content"
>
<p>
Message content
</p>
</div>
</div>
`;
exports[`Message rendering with type 1`] = `
<div
class="message error"
>
<div
class="header"
>
<svg />
<h2>
Testing
</h2>
</div>
<div
class="content"
>
<p>
Message content
</p>
</div>
</div>
`;

View File

@ -0,0 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import style from './style.css';
import InfoIcon from 'feather-icons/dist/icons/info.svg';
import ErrorIcon from 'feather-icons/dist/icons/alert-octagon.svg';
import WarningIcon from 'feather-icons/dist/icons/alert-triangle.svg';
const iconTypes = {
info: InfoIcon,
error: ErrorIcon,
warning: WarningIcon
};
const renderIcon = (type, icon) => {
icon = icon || iconTypes[type];
if (!icon) {
return;
}
const Icon = icon;
return <Icon/>;
};
const Message = ({ type, icon, heading, children }) => (
<div className={ [ style.message, type && style[type] ].filter(Boolean).join(' ') }>
<div className={ style.header }>
{ renderIcon(type, icon) }
<h2>{ heading }</h2>
</div>
<div className={ style.content }>
{ children }
</div>
</div>
);
Message.propTypes = {
type: PropTypes.oneOf([
'info',
'error',
'warning'
]),
icon: PropTypes.oneOfType([
PropTypes.element,
PropTypes.func
]),
heading: PropTypes.string.isRequired,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
};
export default Message;

View File

@ -0,0 +1,72 @@
@import url('../../globals.css');
.message {
background: var(--color-tan);
color: var(--color-black);
margin: var(--spacing-margin) 0;
box-shadow: 0 0 1rem color(var(--color-black) alpha(0.7));
& .header {
& h2 {
display: inline-block;
padding: var(--spacing-margin);
line-height: 2.8rem;
margin: 0;
}
& svg {
padding: var(--spacing-margin);
height: 2.8rem;
width: 2.8rem;
vertical-align: bottom;
}
}
& .content {
padding: var(--spacing-margin);
}
& p {
margin-top: 0;
margin-bottom: var(--spacing-margin) 0;
}
& p:last-child {
margin-bottom: 0;
}
&.error {
& .header {
background: var(--color-red);
color: var(--color-white);
& svg {
background: var(--color-white);
color: var(--color-red);
}
}
}
&.warning {
& .header {
background: var(--color-orange);
}
& svg {
background: var(--color-black);
color: var(--color-orange);
}
}
&.info {
& .header {
background: var(--color-blue);
color: var(--color-white);
}
& svg {
background: var(--color-white);
color: var(--color-blue);
}
}
}

View File

@ -0,0 +1,34 @@
import React from 'react';
import { shallow, render } from 'enzyme';
import Message from 'components/Message';
describe('Message', () => {
test('rendering', () => {
const component = shallow(
<Message heading="Testing" className="testing">
<p>Message content</p>
</Message>
);
expect(component).toMatchSnapshot();
});
test('rendering with icon', () => {
const Icon = () => 'Sample icon SVG';
const component = render(
<Message heading="Testing" icon={ Icon }>
<p>Message content</p>
</Message>
);
expect(component).toMatchSnapshot();
});
test('rendering with type', () => {
const component = render(
<Message heading="Testing" type="error">
<p>Message content</p>
</Message>
);
expect(component).toMatchSnapshot();
});
});

View File

@ -0,0 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RavenBoundary rendering (with error) 1`] = `<Child />`;
exports[`RavenBoundary rendering (with error) 2`] = `
<Translate(RavenError)
details={
Object {
"extra": Object {
"details": "test details",
},
}
}
error={
Object {
"error": "test error",
}
}
/>
`;
exports[`RavenBoundary rendering 1`] = `<Child />`;

View File

@ -0,0 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import RavenError from 'components/RavenError';
class RavenBoundary extends React.Component {
state = {
error: null
}
componentDidCatch(error, errorInfo) {
this.setState({ error, errorInfo });
}
render() {
const { error, errorInfo } = this.state;
const { children } = this.props;
if (error) {
const errorProps = {
details: { extra: errorInfo },
error
};
return <RavenError { ...errorProps }/>;
} else {
return children;
}
}
}
RavenBoundary.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
};
export default RavenBoundary;

View File

@ -0,0 +1,32 @@
import React from 'react';
import { shallow } from 'enzyme';
import RavenBoundary from 'components/RavenBoundary';
const testError = { error: 'test error' };
const testDetails = { details: 'test details' };
describe('RavenBoundary', () => {
test('rendering', () => {
const Child = () => <b>Child</b>;
const component = shallow(
<RavenBoundary>
<Child/>
</RavenBoundary>
);
expect(component).toMatchSnapshot();
});
test('rendering (with error)', () => {
const Child = () => <b>Child</b>;
const component = shallow(
<RavenBoundary>
<Child/>
</RavenBoundary>
);
expect(component).toMatchSnapshot();
component.instance().componentDidCatch(testError, testDetails);
component.update();
expect(component).toMatchSnapshot();
});
});

View File

@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RavenError rendering 1`] = `
<Message
heading="translate(An error has occurred)"
type="error"
>
<p>
<Trans
i18nKey="This error has been logged"
>
This error has been logged. You may also
<a
href="#error-report"
onClick={[Function]}
>
fill out a report
</a>
.
</Trans>
</p>
</Message>
`;

View File

@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate, Trans } from 'react-i18next';
import Raven from 'raven-js';
import Message from 'components/Message';
class RavenError extends React.Component {
componentDidMount() {
const { error, details } = this.props;
Raven.captureException(error, details);
}
reportError = event => {
event.preventDefault();
if (Raven.lastEventId()) {
Raven.showReportDialog();
}
}
render() {
const { t } = this.props;
return <Message type="error" heading={ t('An error has occurred') }>
<p><Trans i18nKey="This error has been logged">
This error has been logged. You may also <a href="#error-report" onClick={ this.reportError }>fill out a report</a>.
</Trans></p>
</Message>;
}
}
RavenError.propTypes = {
error: PropTypes.object.isRequired,
details: PropTypes.object.isRequired,
t: PropTypes.func
};
export default translate()(RavenError);
export { RavenError };

View File

@ -0,0 +1,65 @@
jest.mock('raven-js');
import React from 'react';
import { shallow } from 'enzyme';
import Raven from 'raven-js';
import { RavenError } from 'components/RavenError';
import { translate } from '__mocks__/i18n';
const testError = { error: 'test error' };
const testDetails = { details: 'test details' };
describe('RavenError', () => {
test('rendering', () => {
const component = shallow(
<RavenError
error={ testError }
details={ testDetails }
t={ translate }/>
);
expect(component).toMatchSnapshot();
});
test('captures exception', () => {
shallow(
<RavenError
error={ testError }
details={ testDetails }
t={ translate }/>
);
expect(Raven.captureException).toHaveBeenCalledWith(testError, testDetails);
});
describe('error reporting', () => {
test('clicking to fill out a report when an event has been logged', () => {
Raven.lastEventId.mockReturnValue(1);
const component = shallow(
<RavenError
error={ testError }
details={ testDetails }
t={ translate }/>
);
const eventObj = { preventDefault: jest.fn() };
component.find('a').simulate('click', eventObj);
expect(eventObj.preventDefault).toHaveBeenCalled();
expect(Raven.showReportDialog).toHaveBeenCalled();
});
test('clicking to fill out a report when an event has not been logged', () => {
Raven.lastEventId.mockReturnValue(false);
const component = shallow(
<RavenError
error={ testError }
details={ testDetails }
t={ translate }/>
);
const eventObj = { preventDefault: jest.fn() };
component.find('a').simulate('click', eventObj);
expect(eventObj.preventDefault).toHaveBeenCalled();
expect(Raven.showReportDialog).not.toHaveBeenCalled();
});
});
});

80
src/components/SVG/Box.js Normal file
View File

@ -0,0 +1,80 @@
import React from 'react';
import PropTypes from 'prop-types';
import style from './style';
import reflowable from './reflowable';
@reflowable
class Box extends React.PureComponent {
static defaultProps = {
padding: 5,
radius: 3
}
label = React.createRef()
children = [React.createRef()]
reflow() {
const { padding, useAnchors } = this.props;
const box = this.children[0].current.getBBox();
const labelBox = this.label.current ? this.label.current.getBBox() : { width: 0, height: 0};
this.setBBox({
width: Math.max(box.width + 2 * padding, labelBox.width),
height: box.height + 2 * padding + labelBox.height,
axisY: (useAnchors ? box.axisY : box.height / 2) + padding + labelBox.height,
axisX1: useAnchors ? box.axisX1 + padding : 0,
axisX2: useAnchors ? box.axisX2 + padding : box.width + 2 * padding
});
this.setStateAsync({
width: this.getBBox().width,
height: box.height + 2 * padding,
contentTransform: `translate(${ padding } ${ padding + labelBox.height })`,
rectTransform: `translate(0 ${ labelBox.height })`,
labelTransform: `translate(0 ${ labelBox.height })`
});
}
render() {
const { theme, radius, label, children } = this.props;
const { width, height, labelTransform, rectTransform, contentTransform } = this.state || {};
const rectProps = {
style: style[theme],
width,
height,
rx: radius,
ry: radius,
transform: rectTransform
};
const textProps = {
transform: labelTransform,
style: style.infoText,
ref: this.label
};
return <React.Fragment>
<rect { ...rectProps } ></rect>
{ label && <text { ...textProps }>{ label }</text> }
<g transform={ contentTransform }>
{ React.cloneElement(React.Children.only(children), {
ref: this.children[0]
}) }
</g>
</React.Fragment>;
}
}
Box.propTypes = {
children: PropTypes.node,
label: PropTypes.string,
padding: PropTypes.number,
useAnchors: PropTypes.bool,
radius: PropTypes.number,
theme: PropTypes.string
};
export default Box;

View File

@ -0,0 +1,53 @@
import React from 'react';
import { mount } from 'enzyme';
import Box from './Box';
import SVGElement from '__mocks__/SVGElement';
const originalGetBBox = window.Element.prototype.getBBox;
describe('Box', () => {
beforeEach(() => {
window.Element.prototype.getBBox = function() {
return { width: 100, height: 10 };
};
});
afterEach(() => {
window.Element.prototype.getBBox = originalGetBBox;
});
test('rendering', async () => {
const component = mount(
<Box>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Box>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
test('rendering with content anchors', async () => {
const component = mount(
<Box useAnchors>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Box>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
test('rendering with label', async () => {
const component = mount(
<Box label="Test label">
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Box>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
});

View File

@ -0,0 +1,97 @@
import React from 'react';
import PropTypes from 'prop-types';
import { List } from 'immutable';
import style from './style';
import reflowable from './reflowable';
import Path from './path';
@reflowable
class HorizontalLayout extends React.PureComponent {
static defaultProps = {
withConnectors: false,
spacing: 10
}
state = {
childTransforms: List()
}
children = []
updateChildTransforms(childBoxes) {
return this.state.childTransforms.withMutations(transforms => (
childBoxes.forEach((box, i) => (
transforms.set(i, `translate(${ box.offsetX } ${ box.offsetY })`)
))
));
}
updateConnectorPaths(childBoxes) {
let last = childBoxes[0];
return childBoxes.slice(1).reduce((path, box) => {
try {
return path
.moveTo({ x: last.offsetX + last.axisX2, y: this.getBBox().axisY })
.lineTo({ x: box.offsetX + box.axisX1 });
}
finally {
last = box;
}
}, new Path()).toString();
}
reflow() {
const { spacing, withConnectors } = this.props;
const childBoxes = this.children.map(child => child.current.getBBox());
const verticalCenter = childBoxes.reduce((center, box) => Math.max(center, box.axisY), 0);
const width = childBoxes.reduce((width, box) => width + box.width, 0) + (childBoxes.length - 1) * spacing;
const height = childBoxes.reduce((ascHeight, box) => Math.max(ascHeight, box.axisY), 0) +
childBoxes.reduce((decHeight, box) => Math.max(decHeight, box.height - box.axisY), 0);
this.setBBox({ width, height, axisY: verticalCenter }, { axisX1: true, axisX2: true });
let offset = 0;
childBoxes.forEach(box => {
box.offsetX = offset;
box.offsetY = this.getBBox().axisY - box.axisY;
offset += box.width + spacing;
});
this.setStateAsync({
childTransforms: this.updateChildTransforms(childBoxes),
connectorPaths: withConnectors ? this.updateConnectorPaths(childBoxes) : ''
});
}
render() {
const { children } = this.props;
const { childTransforms, connectorPaths } = this.state;
this.makeRefCollection(this.children, React.Children.count(children));
return <React.Fragment>
<path d={ connectorPaths } style={ style.connectors }></path>
{ React.Children.map(children, (child, i) => (
<g transform={ childTransforms.get(i) }>
{ React.cloneElement(child, {
ref: this.children[i]
}) }
</g>
))}
</React.Fragment>;
}
}
HorizontalLayout.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
spacing: PropTypes.number,
withConnectors: PropTypes.bool
};
export default HorizontalLayout;

View File

@ -0,0 +1,34 @@
import React from 'react';
import { mount } from 'enzyme';
import HorizontalLayout from './HorizontalLayout';
import SVGElement from '__mocks__/SVGElement';
describe('HorizontalLayout', () => {
test('rendering', async () => {
const component = mount(
<HorizontalLayout>
<SVGElement bbox={{ width: 100, height: 100 }}/>
<SVGElement bbox={{ width: 100, height: 100 }}/>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</HorizontalLayout>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
test('rendering with connectors', async () => {
const component = mount(
<HorizontalLayout withConnectors>
<SVGElement bbox={{ width: 100, height: 100 }}/>
<SVGElement bbox={{ width: 100, height: 100 }}/>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</HorizontalLayout>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
});

100
src/components/SVG/Image.js Normal file
View File

@ -0,0 +1,100 @@
import React from 'react';
import PropTypes from 'prop-types';
import style from './style';
import reflowable from './reflowable';
const namespaceProps = {
'xmlns': 'http://www.w3.org/2000/svg',
'xmlns:cc': 'http://creativecommons.org/ns#',
'xmlns:rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
};
const metadata = `<rdf:rdf>
<cc:license rdf:about="http://creativecommons.org/licenses/by/3.0/">
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"></cc:permits>
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"></cc:permits>
<cc:requires rdf:resource="http://creativecommons.org/ns#Notice"></cc:requires>
<cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"></cc:requires>
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"></cc:permits>
</cc:license>
</rdf:rdf>`;
@reflowable
class Image extends React.PureComponent {
static defaultProps = {
padding: 10
}
state = {
width: 0,
height: 0
}
svg = React.createRef()
children = [React.createRef()]
async svgUrl(type) {
const markup = this.svg.current.outerHTML;
return new Blob([markup], { type });
}
async pngUrl(type) {
const markup = this.svg.current.outerHTML;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const loader = new window.Image(); // Using window.Image to avoid name conflict :(
loader.width = canvas.width = Number(this.svg.current.getAttribute('width')) * 2;
loader.height = canvas.height = Number(this.svg.current.getAttribute('height')) * 2;
await new Promise(resolve => {
loader.onload = resolve;
loader.src = 'data:image/svg+xml,' + encodeURIComponent(markup);
});
context.drawImage(loader, 0, 0, loader.width, loader.height);
return new Promise(resolve => canvas.toBlob(resolve, type));
}
reflow() {
const { padding } = this.props;
const box = this.children[0].current.getBBox();
this.setStateAsync({
width: Math.round(box.width + 2 * padding),
height: Math.round(box.height + 2 * padding)
});
}
render() {
const { width, height } = this.state;
const { padding, children } = this.props;
const svgProps = {
width,
height,
viewBox: [0, 0, width, height].join(' '),
style: style.image,
ref: this.svg,
...namespaceProps
};
return <svg { ...svgProps }>
<metadata dangerouslySetInnerHTML={{ __html: metadata }}></metadata>
<g transform={ `translate(${ padding } ${ padding })` }>
{ React.cloneElement(React.Children.only(children), {
ref: this.children[0]
}) }
</g>
</svg>;
}
}
Image.propTypes = {
children: PropTypes.node,
padding: PropTypes.number
};
export default Image;

View File

@ -0,0 +1,19 @@
import React from 'react';
import { mount } from 'enzyme';
import Image from './Image';
import SVGElement from '__mocks__/SVGElement';
describe('Image', () => {
test('rendering', async () => {
const component = mount(
<Image>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Image>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
});

139
src/components/SVG/Loop.js Normal file
View File

@ -0,0 +1,139 @@
import React from 'react';
import PropTypes from 'prop-types';
import style from './style';
import reflowable from './reflowable';
import Path from './path';
const skipPath = (box, greedy) => {
const vert = Math.max(0, box.axisY - 10);
const horiz = box.width - 10;
let path = new Path({ relative: true });
if (!greedy) {
path
.moveTo({ x: 10, y: box.axisY + box.offsetY - 15, relative: false })
.lineTo({ x: 5, y: 5 })
.moveTo({ x: -5, y: -5 })
.lineTo({ x: -5, y: 5 });
}
return path
.moveTo({ x: 0, y: box.axisY + box.offsetY, relative: false })
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: -10 })
.lineTo({ y: -vert })
.quadraticCurveTo({ cx: 0, cy: -10, x: 10, y: -10 })
.lineTo({ x: horiz })
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: 10 })
.lineTo({ y: vert })
.quadraticCurveTo({ cx: 0, cy: 10, x: 10, y: 10 });
};
const repeatPath = (box, greedy) => {
const vert = box.height - box.axisY - 10;
let path = new Path({ relative: true });
if (greedy) {
path
.moveTo({ x: box.offsetX + box.width + 10, y: box.axisY + box.offsetY + 15, relative: false })
.lineTo({ x: 5, y: -5 })
.moveTo({ x: -5, y: 5 })
.lineTo({ x: -5, y: -5 });
}
return path
.moveTo({ x: box.offsetX, y: box.axisY + box.offsetY, relative: false })
.quadraticCurveTo({ cx: -10, cy: 0, x: -10, y: 10 })
.lineTo({ y: vert })
.quadraticCurveTo({ cx: 0, cy: 10, x: 10, y: 10 })
.lineTo({ x: box.width })
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: -10 })
.lineTo({ y: -vert })
.quadraticCurveTo({ cx: 0, cy: -10, x: -10, y: -10 });
};
@reflowable
class Loop extends React.PureComponent {
label = React.createRef()
children = [React.createRef()]
get contentOffset() {
const { skip, repeat } = this.props;
if (skip) {
return { x: 15, y: 10 };
} else if (repeat) {
return { x: 10, y: 0 };
} else {
return { x: 0, y: 0 };
}
}
reflow() {
const { skip, repeat, greedy } = this.props;
const box = this.children[0].current.getBBox();
const labelBox = this.label.current ? this.label.current.getBBox() : { width: 0, height: 0 };
let height = box.height + labelBox.height;
if (skip) {
height += 10;
}
if (repeat) {
height += 10;
}
this.setBBox({
width: box.width + this.contentOffset.x * 2,
height,
axisY: box.axisY + this.contentOffset.y,
axisX1: box.axisX1 + this.contentOffset.x,
axisX2: box.axisX2 + this.contentOffset.x
});
box.offsetX = this.contentOffset.x;
box.offsetY = this.contentOffset.y;
this.setStateAsync({
labelTransform: `translate(${ this.getBBox().width - labelBox.width - 10 } ${ this.getBBox().height + 2 })`,
loopPaths: [
skip && skipPath(box, greedy),
repeat && repeatPath(box, greedy)
].filter(Boolean).join('')
});
}
render() {
const { label, children } = this.props;
const { loopPaths, labelTransform } = this.state || {};
const textProps = {
transform: labelTransform,
style: style.infoText,
ref: this.label
};
return <React.Fragment>
<path d={ loopPaths } style={ style.connectors }></path>
{ label && <text { ...textProps }>{ label }</text> }
<g transform={ `translate(${ this.contentOffset.x } ${ this.contentOffset.y })` }>
{ React.cloneElement(React.Children.only(children), {
ref: this.children[0]
}) }
</g>
</React.Fragment>;
}
}
Loop.propTypes = {
children: PropTypes.node.isRequired,
greedy: PropTypes.bool,
label: PropTypes.string,
skip: PropTypes.bool,
repeat: PropTypes.bool
};
export default Loop;

View File

@ -0,0 +1,86 @@
import React from 'react';
import { mount } from 'enzyme';
import Loop from './Loop';
import SVGElement from '__mocks__/SVGElement';
const originalGetBBox = window.Element.prototype.getBBox;
describe('Loop', () => {
beforeEach(() => {
window.Element.prototype.getBBox = function() {
return { width: 100, height: 10 };
};
});
afterEach(() => {
window.Element.prototype.getBBox = originalGetBBox;
});
test('rendering', async () => {
const component = mount(
<Loop>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Loop>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
test('rendering with skip path', async () => {
const component = mount(
<Loop skip>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Loop>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
test('rendering with repeat path', async () => {
const component = mount(
<Loop repeat>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Loop>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
test('rendering with repeat path and label', async () => {
const component = mount(
<Loop repeat label="Test label">
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Loop>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
test('rendering with skip and repeat paths', async () => {
const component = mount(
<Loop skip repeat>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Loop>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
test('rendering with greedy skip and repeat paths', async () => {
const component = mount(
<Loop greedy skip repeat>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Loop>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
});

Some files were not shown because too many files have changed in this diff Show More