├── .gitignore
├── code
├── assets
│ ├── icon128.png
│ ├── icon16.png
│ └── icon48.png
├── vendor
│ ├── fonts
│ │ ├── Roboto-Regular.woff2
│ │ ├── RobotoMono-Regular.woff2
│ │ └── MaterialIcons-Regular.woff2
│ └── css
│ │ └── darkula.css
├── html
│ ├── devtools.html
│ └── panel.html
├── js
│ ├── modules
│ │ └── helper.js
│ ├── devtools.js
│ ├── libs
│ │ ├── languages
│ │ │ └── php.js
│ │ └── highlight.js
│ ├── background.js
│ ├── panel.js
│ └── content.js
├── manifest.json
└── css
│ └── panel.css
├── lint-options.json
├── README.md
├── package.json
└── Gruntfile.js
/.gitignore:
--------------------------------------------------------------------------------
1 | key.pem
2 | node_modules
3 | build/*.crx
4 | build/unpacked-dev
5 | build/unpacked-prod
6 |
--------------------------------------------------------------------------------
/code/assets/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mpociot/laravel-testtools/HEAD/code/assets/icon128.png
--------------------------------------------------------------------------------
/code/assets/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mpociot/laravel-testtools/HEAD/code/assets/icon16.png
--------------------------------------------------------------------------------
/code/assets/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mpociot/laravel-testtools/HEAD/code/assets/icon48.png
--------------------------------------------------------------------------------
/code/vendor/fonts/Roboto-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mpociot/laravel-testtools/HEAD/code/vendor/fonts/Roboto-Regular.woff2
--------------------------------------------------------------------------------
/code/vendor/fonts/RobotoMono-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mpociot/laravel-testtools/HEAD/code/vendor/fonts/RobotoMono-Regular.woff2
--------------------------------------------------------------------------------
/code/vendor/fonts/MaterialIcons-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mpociot/laravel-testtools/HEAD/code/vendor/fonts/MaterialIcons-Regular.woff2
--------------------------------------------------------------------------------
/code/html/devtools.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/code/js/modules/helper.js:
--------------------------------------------------------------------------------
1 | exports.ucfirst = function(str) {
2 | str += '';
3 | var f = str.charAt(0)
4 | .toUpperCase();
5 | return f + str.substr(1);
6 | };
7 |
8 | exports.addslashes = function( str ) {
9 | return (str + '').replace(/[\\"']/g, '\\$&').replace(/\u0000/g, '\\0');
10 | };
11 |
--------------------------------------------------------------------------------
/lint-options.json:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 | "curly": true,
4 | "eqeqeq": true,
5 | "immed": true,
6 | "latedef": true,
7 | "newcap": true,
8 | "noarg": true,
9 | "nonew": true,
10 | "undef": true,
11 | "evil": true,
12 | "trailing": true,
13 | "boss": true,
14 | "eqnull": true,
15 | "globals": {
16 | "FileReader": true,
17 | "beforeEach": true,
18 | "describe": true,
19 | "afterEach": true,
20 | "it": true,
21 | "chrome": true,
22 | "alert": true,
23 | "window": true,
24 | "document": true,
25 | "_postMessage": true
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/code/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "Laravel TestTools",
4 | "description": "Create Laravel integration tests while surfing your website.",
5 | "version": "0.1.2",
6 | "devtools_page": "html/devtools.html",
7 | "content_scripts": [
8 | {
9 | "matches": [""],
10 | "js": ["js/content.js"],
11 | "run_at": "document_end"
12 | }
13 | ],
14 | "icons": {
15 | "16": "assets/icon16.png",
16 | "48": "assets/icon48.png",
17 | "128": "assets/icon128.png"
18 | },
19 | "background": {
20 | "scripts": ["js/background.js"]
21 | },
22 | "permissions": ["", "contextMenus", "storage", "clipboardWrite"]
23 | }
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laravel TestTools
2 |
3 |
4 | Check out the [introduction post](http://marcelpociot.de/blog/2016-03-21-laravel-testtools) about the chrome extension.
5 |
6 | ## Installation
7 |
8 | ```
9 | git clone git@github.com:mpociot/laravel-testtools.git
10 |
11 | # in case you don't have Grunt yet:
12 | sudo npm install -g grunt-cli
13 | ```
14 |
15 | ## Build instructions
16 |
17 | ```
18 | cd laravel-testtools
19 | npm install
20 | grunt
21 | ```
22 |
23 | When the grunt command is finished, you can use the `build/unpacked-dev/` folder to use as the developer version of the extension.
24 |
25 | ## License
26 |
27 | Laravel TestTools is free software distributed under the terms of the MIT license.
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "LaravelTestTools",
3 | "version": "0.3.0",
4 | "description": "Chrome extension to generate Laravel integration tests while using your app.",
5 | "dependencies": {},
6 | "devDependencies": {
7 | "browserify": "^13.0.0",
8 | "faker": "^3.1.0",
9 | "grunt": "*",
10 | "grunt-browserify": "^5.0.0",
11 | "grunt-contrib-clean": "^1.0.0",
12 | "grunt-contrib-copy": "^1.0.0",
13 | "grunt-contrib-jshint": "^1.0.0",
14 | "grunt-contrib-uglify": "^1.0.1",
15 | "grunt-contrib-watch": "^1.0.0",
16 | "grunt-crx": "^1.0.3",
17 | "grunt-mkdir": "^1.0.0",
18 | "jquery": "^2.2.2",
19 | "vue": "1.0.24-csp"
20 | },
21 | "engines": {
22 | "node": ">= 0.10.0"
23 | },
24 | "scripts": {
25 | "watch": "grunt watch"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/code/vendor/css/darkula.css:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Darkula color scheme from the JetBrains family of IDEs
4 |
5 | */
6 |
7 |
8 | .hljs {
9 | display: block;
10 | overflow-x: auto;
11 | padding: 0.5em;
12 | background: #2b2b2b;
13 | }
14 |
15 | .hljs {
16 | color: #bababa;
17 | }
18 |
19 | .hljs-strong,
20 | .hljs-emphasis {
21 | color: #a8a8a2;
22 | }
23 |
24 | .hljs-bullet,
25 | .hljs-quote,
26 | .hljs-link,
27 | .hljs-number,
28 | .hljs-regexp,
29 | .hljs-literal {
30 | color: #6896ba;
31 | }
32 |
33 | .hljs-code,
34 | .hljs-selector-class {
35 | color: #a6e22e;
36 | }
37 |
38 | .hljs-emphasis {
39 | font-style: italic;
40 | }
41 |
42 | .hljs-keyword,
43 | .hljs-selector-tag,
44 | .hljs-section,
45 | .hljs-attribute,
46 | .hljs-name,
47 | .hljs-variable {
48 | color: #cb7832;
49 | }
50 |
51 | .hljs-params {
52 | color: #b9b9b9;
53 | }
54 |
55 | .hljs-string,
56 | .hljs-subst,
57 | .hljs-type,
58 | .hljs-built_in,
59 | .hljs-builtin-name,
60 | .hljs-symbol,
61 | .hljs-selector-id,
62 | .hljs-selector-attr,
63 | .hljs-selector-pseudo,
64 | .hljs-template-tag,
65 | .hljs-template-variable,
66 | .hljs-addition {
67 | color: #e0c46c;
68 | }
69 |
70 | .hljs-comment,
71 | .hljs-deletion,
72 | .hljs-meta {
73 | color: #7f7f7f;
74 | }
75 |
--------------------------------------------------------------------------------
/code/js/devtools.js:
--------------------------------------------------------------------------------
1 | // Create a connection to the background page
2 | var backgroundPageConnection = chrome.runtime.connect({
3 | name: "panel"
4 | });
5 |
6 | var tab_id = chrome.devtools.inspectedWindow.tabId;
7 |
8 | backgroundPageConnection.postMessage({
9 | name: 'init',
10 | tabId: chrome.devtools.inspectedWindow.tabId
11 | });
12 |
13 | chrome.devtools.panels.create("Laravel TestTools", null, "../html/panel.html", function(extensionPanel) {
14 | var _window;
15 | var steps;
16 |
17 | extensionPanel.onShown.addListener(function tmp(panelWindow) {
18 | extensionPanel.onShown.removeListener(tmp); // Run once
19 | _window = panelWindow;
20 |
21 | _window._postMessage = function(obj) {
22 | backgroundPageConnection.postMessage({
23 | "name": "postMessage",
24 | "tabId": tab_id,
25 | "object": obj
26 | });
27 | };
28 |
29 | // Initialize
30 | if (steps) {
31 | _window.setSteps(steps);
32 | } else {
33 | _window._postMessage({
34 | "method": "getSteps"
35 | });
36 | }
37 |
38 |
39 | chrome.devtools.inspectedWindow.eval(
40 | "window.location.pathname",
41 | function(result) {
42 | _window.setPathname(result);
43 | }
44 | );
45 | });
46 |
47 | backgroundPageConnection.onMessage.addListener(function (message) {
48 | if (message.factories) {
49 | backgroundPageConnection.postMessage({
50 | "name": "setFactories",
51 | "tabId": tab_id,
52 | "factories": message.factories
53 | });
54 | } else {
55 | if (_window) {
56 | _window.setSteps(message);
57 | } else {
58 | steps = message;
59 | }
60 | }
61 | });
62 | }
63 | );
64 |
--------------------------------------------------------------------------------
/code/css/panel.css:
--------------------------------------------------------------------------------
1 |
2 | @font-face {
3 | font-family: 'Material Icons';
4 | font-style: normal;
5 | font-weight: 400;
6 | src: local('Material Icons'),
7 | local('MaterialIcons-Regular'),
8 | url(../vendor/fonts/MaterialIcons-Regular.woff2) format('woff2');
9 | }
10 |
11 | @font-face {
12 | font-family: 'Roboto';
13 | font-style: normal;
14 | font-weight: 400;
15 | src: local('Roboto'),
16 | local('Roboto-Regular'),
17 | url(../vendor/fonts/Roboto-Regular.woff2) format('woff2');
18 | }
19 |
20 | @font-face {
21 | font-family: 'Roboto Mono';
22 | font-style: normal;
23 | font-weight: 400;
24 | src: local('Roboto'),
25 | local('RobotoMono-Regular'),
26 | url(../vendor/fonts/RobotoMono-Regular.woff2) format('woff2');
27 | }
28 |
29 |
30 | body, pre, code {
31 | margin: 0;
32 | padding: 0;
33 | background: #2b2b2b;
34 | font-size: 15px;
35 | font-family: Roboto;
36 | }
37 |
38 | pre, code {
39 | font-family: "Roboto Mono";
40 | font-size: 13px;
41 | }
42 |
43 | .header .material-icons.recording {
44 | color: red;
45 | }
46 |
47 | .header {
48 | position: fixed;
49 | z-index: 2;
50 | width: 100%;
51 | height: 50px;
52 | border-bottom: 1px solid #e3e3e3;
53 | box-shadow: 0 0 8px rgba(0,0,0,0.15);
54 | font-size: 13px;
55 | background: #fff;
56 | }
57 | .header a,
58 | .header .material-icons {
59 | display: inline-block;
60 | vertical-align: middle;
61 | }
62 | .header .material-icons {
63 | margin-right: 3px;
64 | position: relative;
65 | top: -1px;
66 | color: #999;
67 | }
68 | .button {
69 | float: left;
70 | position: relative;
71 | z-index: 1;
72 | cursor: pointer;
73 | height: 50px;
74 | line-height: 50px;
75 | border-left: 1px solid #e3e3e3;
76 | border-bottom: 1px solid #e3e3e3;
77 | background-color: #fff;
78 | font-size: 13px;
79 | color: #666;
80 | padding: 0 22px 0 20px;
81 | transition: box-shadow 0.25s ease, border-color 0.5s ease;
82 | }
83 | .button:hover {
84 | box-shadow: 0 2px 12px rgba(0,0,0,0.1);
85 | }
86 | .button:active {
87 | box-shadow: 0 2px 16px rgba(0,0,0,0.25);
88 | }
89 | .button.active {
90 | border-bottom: 2px solid #44A1FF;
91 | }
92 | .container {
93 | padding-top: 50px;
94 | position: relative;
95 | z-index: 1;
96 | height: 100%;
97 | }
98 | .message-container {
99 | display: inline-block;
100 | height: 50px;
101 | line-height: 50px;
102 | cursor: default;
103 | margin-left: 20px;
104 | }
105 |
106 | .message {
107 | color: #44A1FF;
108 | transition: all .3s ease;
109 | display: inline-block;
110 | position: absolute;
111 | }
112 |
113 | .material-icons {
114 | font-family: 'Material Icons';
115 | font-weight: normal;
116 | font-style: normal;
117 | font-size: 24px; /* Preferred icon size */
118 | display: inline-block;
119 | line-height: 1;
120 | text-transform: none;
121 | letter-spacing: normal;
122 | word-wrap: normal;
123 | white-space: nowrap;
124 | direction: ltr;
125 |
126 | /* Support for all WebKit browsers. */
127 | -webkit-font-smoothing: antialiased;
128 | /* Support for Safari and Chrome. */
129 | text-rendering: optimizeLegibility;
130 | }
131 |
132 | #testbar {
133 | padding: 20px 0 0 0;
134 | }
135 |
136 | #settings, #toolbar {
137 | top: 50px;
138 | position: fixed;
139 | width: 100%;
140 | background-color: #fff;
141 | padding: 20px;
142 | }
143 |
144 | #toolbar label, #settings label {
145 | display: inline-block;
146 | width: 200px;
147 | min-height: 30px;
148 | line-height: 30px;
149 | }
150 |
151 | input[type="text"] {
152 | border: none;
153 | box-shadow: 0 0 4px rgba(0,0,0,0.6);
154 | font-size: 13px;
155 | width: 200px;
156 | height: 25px;
157 | margin: 0 0 5px 15px;
158 | }
159 |
--------------------------------------------------------------------------------
/code/js/libs/languages/php.js:
--------------------------------------------------------------------------------
1 | module.exports = function(hljs) {
2 | var VARIABLE = {
3 | begin: '\\$+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'
4 | };
5 | var PREPROCESSOR = {
6 | className: 'meta', begin: /<\?(php)?|\?>/
7 | };
8 | var STRING = {
9 | className: 'string',
10 | contains: [hljs.BACKSLASH_ESCAPE, PREPROCESSOR],
11 | variants: [
12 | {
13 | begin: 'b"', end: '"'
14 | },
15 | {
16 | begin: 'b\'', end: '\''
17 | },
18 | hljs.inherit(hljs.APOS_STRING_MODE, {illegal: null}),
19 | hljs.inherit(hljs.QUOTE_STRING_MODE, {illegal: null})
20 | ]
21 | };
22 | var NUMBER = {variants: [hljs.BINARY_NUMBER_MODE, hljs.C_NUMBER_MODE]};
23 | return {
24 | aliases: ['php3', 'php4', 'php5', 'php6'],
25 | case_insensitive: true,
26 | keywords:
27 | 'and include_once list abstract global private echo interface as static endswitch ' +
28 | 'array null if endwhile or const for endforeach self var while isset public ' +
29 | 'protected exit foreach throw elseif include __FILE__ empty require_once do xor ' +
30 | 'return parent clone use __CLASS__ __LINE__ else break print eval new ' +
31 | 'catch __METHOD__ case exception default die require __FUNCTION__ ' +
32 | 'enddeclare final try switch continue endfor endif declare unset true false ' +
33 | 'trait goto instanceof insteadof __DIR__ __NAMESPACE__ ' +
34 | 'yield finally',
35 | contains: [
36 | hljs.HASH_COMMENT_MODE,
37 | hljs.COMMENT('//', '$', {contains: [PREPROCESSOR]}),
38 | hljs.COMMENT(
39 | '/\\*',
40 | '\\*/',
41 | {
42 | contains: [
43 | {
44 | className: 'doctag',
45 | begin: '@[A-Za-z]+'
46 | }
47 | ]
48 | }
49 | ),
50 | hljs.COMMENT(
51 | '__halt_compiler.+?;',
52 | false,
53 | {
54 | endsWithParent: true,
55 | keywords: '__halt_compiler',
56 | lexemes: hljs.UNDERSCORE_IDENT_RE
57 | }
58 | ),
59 | {
60 | className: 'string',
61 | begin: /<<<['"]?\w+['"]?$/, end: /^\w+;?$/,
62 | contains: [
63 | hljs.BACKSLASH_ESCAPE,
64 | {
65 | className: 'subst',
66 | variants: [
67 | {begin: /\$\w+/},
68 | {begin: /\{\$/, end: /\}/}
69 | ]
70 | }
71 | ]
72 | },
73 | PREPROCESSOR,
74 | VARIABLE,
75 | {
76 | // swallow composed identifiers to avoid parsing them as keywords
77 | begin: /(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/
78 | },
79 | {
80 | className: 'function',
81 | beginKeywords: 'function', end: /[;{]/, excludeEnd: true,
82 | illegal: '\\$|\\[|%',
83 | contains: [
84 | hljs.UNDERSCORE_TITLE_MODE,
85 | {
86 | className: 'params',
87 | begin: '\\(', end: '\\)',
88 | contains: [
89 | 'self',
90 | VARIABLE,
91 | hljs.C_BLOCK_COMMENT_MODE,
92 | STRING,
93 | NUMBER
94 | ]
95 | }
96 | ]
97 | },
98 | {
99 | className: 'class',
100 | beginKeywords: 'class interface', end: '{', excludeEnd: true,
101 | illegal: /[:\(\$"]/,
102 | contains: [
103 | {beginKeywords: 'extends implements'},
104 | hljs.UNDERSCORE_TITLE_MODE
105 | ]
106 | },
107 | {
108 | beginKeywords: 'namespace', end: ';',
109 | illegal: /[\.']/,
110 | contains: [hljs.UNDERSCORE_TITLE_MODE]
111 | },
112 | {
113 | beginKeywords: 'use', end: ';',
114 | contains: [hljs.UNDERSCORE_TITLE_MODE]
115 | },
116 | {
117 | begin: '=>' // No markup, just a relevance booster
118 | },
119 | STRING,
120 | NUMBER
121 | ]
122 | };
123 | };
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 |
3 | var pkg = grunt.file.readJSON('package.json');
4 | var mnf = grunt.file.readJSON('code/manifest.json');
5 |
6 | var fileMaps = { browserify: {}, uglify: {} };
7 | var file, files = grunt.file.expand({cwd:'code/js'}, ['*.js']);
8 | for (var i = 0; i < files.length; i++) {
9 | file = files[i];
10 | fileMaps.browserify['build/unpacked-dev/js/' + file] = 'code/js/' + file;
11 | fileMaps.uglify['build/unpacked-prod/js/' + file] = 'build/unpacked-dev/js/' + file;
12 | }
13 |
14 | //
15 | // config
16 | //
17 |
18 | grunt.initConfig({
19 |
20 | clean: ['build/unpacked-dev', 'build/unpacked-prod', 'build/*.crx'],
21 |
22 | mkdir: {
23 | unpacked: { options: { create: ['build/unpacked-dev', 'build/unpacked-prod'] } },
24 | js: { options: { create: ['build/unpacked-dev/js'] } }
25 | },
26 |
27 | jshint: {
28 | options: grunt.file.readJSON('lint-options.json'), // see http://www.jshint.com/docs/options/
29 | all: { src: ['package.json', 'lint-options.json', 'Gruntfile.js', 'code/**/*.js',
30 | 'code/**/*.json', '!code/js/libs/*'] }
31 | },
32 |
33 | copy: {
34 | main: { files: [ {
35 | expand: true,
36 | cwd: 'code/',
37 | src: ['**', '!js/**', '!**/*.md'],
38 | dest: 'build/unpacked-dev/'
39 | } ] },
40 | prod: { files: [ {
41 | expand: true,
42 | cwd: 'build/unpacked-dev/',
43 | src: ['**', '!js/*.js'],
44 | dest: 'build/unpacked-prod/'
45 | } ] },
46 | artifact: { files: [ {
47 | expand: true,
48 | cwd: 'build/',
49 | src: [pkg.name + '-' + pkg.version + '.crx'],
50 | dest: process.env.CIRCLE_ARTIFACTS
51 | } ] }
52 | },
53 |
54 | crx: {
55 | package: {
56 | "src": "build/unpacked-prod/**/*",
57 | "dest": "build/",
58 | }
59 | },
60 |
61 | browserify: {
62 | build: {
63 | files: fileMaps.browserify,
64 | options: { browserifyOptions: {
65 | debug: true, // for source maps
66 | } }
67 | }
68 | },
69 |
70 | uglify: {
71 | min: { files: fileMaps.uglify }
72 | },
73 |
74 | watch: {
75 | js: {
76 | files: ['package.json', 'lint-options.json', 'Gruntfile.js', 'code/**/*.js',
77 | 'code/**/*.json', '!code/js/libs/*'],
78 | tasks: ['test', 'copy:main', 'manifest','mkdir:js', 'browserify', 'copy:prod']
79 | }
80 | }
81 |
82 | });
83 |
84 | grunt.loadNpmTasks('grunt-contrib-clean');
85 | grunt.loadNpmTasks('grunt-mkdir');
86 | grunt.loadNpmTasks('grunt-contrib-jshint');
87 | grunt.loadNpmTasks('grunt-contrib-copy');
88 | grunt.loadNpmTasks('grunt-browserify');
89 | grunt.loadNpmTasks('grunt-contrib-uglify');
90 | grunt.loadNpmTasks('grunt-contrib-watch');
91 | grunt.loadNpmTasks('grunt-crx');
92 |
93 | //
94 | // custom tasks
95 | //
96 |
97 | grunt.registerTask(
98 | 'manifest', 'Extend manifest.json with extra fields from package.json',
99 | function() {
100 | var fields = ['version', 'description'];
101 | for (var i = 0; i < fields.length; i++) {
102 | var field = fields[i];
103 | mnf[field] = pkg[field];
104 | }
105 | grunt.file.write('build/unpacked-dev/manifest.json', JSON.stringify(mnf, null, 4) + '\n');
106 | grunt.log.ok('manifest.json generated');
107 | }
108 | );
109 |
110 | grunt.registerTask(
111 | 'circleci', 'Store built extension as CircleCI arfitact',
112 | function() {
113 | if (process.env.CIRCLE_ARTIFACTS) { grunt.task.run('copy:artifact'); }
114 | else { grunt.log.ok('Not on CircleCI, skipped'); }
115 | }
116 | );
117 |
118 | //
119 | // testing-related tasks
120 | //
121 |
122 | grunt.registerTask('test', ['jshint']);
123 | grunt.registerTask('test-cont', ['test', 'watch']);
124 |
125 | //
126 | // DEFAULT
127 | //
128 |
129 | grunt.registerTask('default', ['clean', 'test', 'mkdir:unpacked', 'copy:main', 'manifest',
130 | 'mkdir:js', 'browserify', 'copy:prod', 'uglify', 'crx', 'circleci']);
131 |
132 | };
133 |
--------------------------------------------------------------------------------
/code/html/panel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | %NAMESPACE%%CLASS_USE_STATEMENTS%
68 | class %CLASSNAME% extends %DUSK%TestCase
69 | {
70 | %ENABLED_TRAITS%%FAKER%
71 | /**
72 | * My test implementation
73 | */
74 | public function test%TESTNAME%()
75 | {
76 |
77 |
78 | {{indent}}$this->{{ step.method }}({{ step.args | implode }});{{linebreak}}{{indent}}$this->{{ step.method }}({{ step.args | implode true }});{{linebreak}}{{indent}}{{ step.action }};{{linebreak}}
79 |
80 |
81 |
82 | {{indent}}$this->browse(function (Browser $browser) {
83 | {{duskIndent}}{{indent}}$browser->{{ step.method }}({{ step.args | implode }});{{linebreak}}{{indent}}$this->{{ step.method }}({{ step.args | implode true }});{{linebreak}}{{indent}}{{ step.action }};{{linebreak}}
84 | {{indent}}});
85 | }
86 | }
87 |
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/code/js/background.js:
--------------------------------------------------------------------------------
1 | var connections = {};
2 |
3 | function seeText(info, tab) {
4 | chrome.tabs.sendRequest(tab.id, {
5 | "method": "seeText",
6 | "text": info.selectionText
7 | });
8 | }
9 |
10 | function waitForText(info, tab) {
11 | chrome.tabs.sendRequest(tab.id, {
12 | "method": "waitForText",
13 | "text": info.selectionText
14 | });
15 | }
16 |
17 | function press(info, tab) {
18 | chrome.tabs.sendRequest(tab.id, {
19 | "method": "press"
20 | });
21 | }
22 |
23 | function visit(info, tab) {
24 | chrome.tabs.sendRequest(tab.id, {
25 | "method": "visit"
26 | });
27 | }
28 |
29 | function seePageIs(info, tab) {
30 | chrome.tabs.sendRequest(tab.id, {
31 | "method": "seePageIs"
32 | });
33 | }
34 |
35 | function fake(info, tab, type) {
36 | chrome.tabs.sendRequest(tab.id, {
37 | "method": "fake",
38 | "type": type
39 | });
40 | }
41 |
42 | function createFactoryModel(info, tab, model) {
43 | chrome.tabs.sendRequest(tab.id, {
44 | "method": "createFactoryModel",
45 | "model": model
46 | });
47 | }
48 |
49 | function importFactories(info, tab) {
50 | chrome.tabs.sendRequest(tab.id, {
51 | "method": "importFactories"
52 | });
53 | }
54 |
55 | function loadMenu(factories) {
56 | chrome.contextMenus.removeAll(function() {
57 | // Create menu items
58 | var parent = chrome.contextMenus.create({"title": "Laravel TestTools", "contexts":["all"]});
59 |
60 | chrome.contextMenus.create({
61 | "title": "Import factories",
62 | "parentId": parent,
63 | "contexts":["all"],
64 | "onclick": importFactories
65 | });
66 |
67 | chrome.contextMenus.create({
68 | "type": "separator",
69 | "parentId": parent,
70 | "contexts":["all"]
71 | });
72 |
73 | chrome.contextMenus.create({
74 | "title": "Visit URL",
75 | "parentId": parent,
76 | "contexts":["all"],
77 | "onclick": visit
78 | });
79 | chrome.contextMenus.create({
80 | "title": "See Page is...",
81 | "parentId": parent,
82 | "contexts":["all"],
83 | "onclick": seePageIs
84 | });
85 | chrome.contextMenus.create({
86 | "title": "See text",
87 | "parentId": parent,
88 | "contexts":["selection"],
89 | "onclick": seeText
90 | });
91 | chrome.contextMenus.create({
92 | "title": "Wait for text",
93 | "parentId": parent,
94 | "contexts":["selection"],
95 | "onclick": waitForText
96 | });
97 | chrome.contextMenus.create({
98 | "title": "Press",
99 | "parentId": parent,
100 | "contexts":["all"],
101 | "onclick": press
102 | });
103 |
104 | if (factories) {
105 | var factoryMenu = chrome.contextMenus.create({
106 | "title": "Factories",
107 | "parentId": parent,
108 | "contexts":["all"]
109 | });
110 |
111 | factories.forEach(function(factory) {
112 | var parentFactory = chrome.contextMenus.create({
113 | "title": factory.name,
114 | "parentId": factoryMenu,
115 | "contexts": ["all"]
116 | });
117 | chrome.contextMenus.create({
118 | "title": "Create model: " + factory.name,
119 | "parentId": parentFactory,
120 | "contexts":["all"],
121 | "onclick": (function(factory){
122 | return function(info,tab){
123 | createFactoryModel(info, tab, factory);
124 | };
125 | }(factory.name))
126 | });
127 | /**
128 | factory.properties.forEach(function(property){
129 | chrome.contextMenus.create({
130 | "title": property.key,
131 | "parentId": parentFactory,
132 | "contexts":["all"],
133 | "onclick": (function(key,value){
134 | return function(info,tab){
135 | createFactoryModel(info, tab, key, value);
136 | };
137 | }(property.key,property.value))
138 | });
139 | });
140 | */
141 |
142 | });
143 | }
144 |
145 | var fakerMenu = chrome.contextMenus.create({
146 | "title": "Faker",
147 | "parentId": parent,
148 | "contexts":["all"]
149 | });
150 |
151 | var availableFaker = [
152 | { type: "email", name: "Email" },
153 | { type: "name", name: "Name" },
154 | { type: "firstname", name: "Firstname" },
155 | { type: "word", name: "Word" },
156 | { type: "url", name: "URL" },
157 | ];
158 |
159 | availableFaker.forEach(function(fakerData){
160 | chrome.contextMenus.create({
161 | "title": fakerData.name,
162 | "parentId": fakerMenu,
163 | "contexts": ["all"],
164 | "onclick": (function(type){
165 | return function(info, tab) {
166 | fake(info,tab,type);
167 | };
168 | }(fakerData.type))
169 | });
170 | });
171 |
172 | });
173 | }
174 |
175 |
176 | loadMenu();
177 |
178 | chrome.runtime.onConnect.addListener(function (port) {
179 |
180 | var extensionListener = function (message, sender, sendResponse) {
181 | // The original connection event doesn't include the tab ID of the
182 | // DevTools page, so we need to send it explicitly.
183 | if (message.name === "init") {
184 | connections[message.tabId] = port;
185 | return;
186 | }
187 |
188 | if (message.name === "postMessage") {
189 | chrome.tabs.sendRequest(message.tabId, message.object);
190 | }
191 |
192 | if (message.name === "setFactories") {
193 | chrome.contextMenus.removeAll(function() {
194 | loadMenu(message.factories);
195 | });
196 | }
197 | };
198 |
199 | // Listen to messages sent from the DevTools page
200 | port.onMessage.addListener(extensionListener);
201 |
202 | port.onDisconnect.addListener(function(port) {
203 | port.onMessage.removeListener(extensionListener);
204 | // Disconnect means -> Dev tools closed. Set recording to false.
205 | var tabs = Object.keys(connections);
206 | for (var i=0, len=tabs.length; i < len; i++) {
207 | if (connections[tabs[i]] === port) {
208 | loadMenu();
209 | chrome.tabs.sendRequest(parseInt(tabs[i]), {
210 | "method": "recording",
211 | "value": false
212 | });
213 | delete connections[tabs[i]];
214 | break;
215 | }
216 | }
217 | });
218 | });
219 |
220 | // Receive message from content script and relay to the devTools page for the
221 | // current tab
222 | chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
223 | // Messages from content scripts should have sender.tab set
224 | if (sender.tab) {
225 | var tabId = sender.tab.id;
226 | if (tabId in connections) {
227 | connections[tabId].postMessage(request);
228 | } else {
229 | console.log("Tab not found in connection list.");
230 | }
231 | } else {
232 | console.log("sender.tab not defined.");
233 | }
234 | return true;
235 | });
236 |
--------------------------------------------------------------------------------
/code/js/panel.js:
--------------------------------------------------------------------------------
1 | var helper = require('./modules/helper'),
2 | faker = require('faker/locale/en_US'),
3 | $ = require('jquery'),
4 | hljs = require('./libs/highlight'),
5 | Vue = require('vue');
6 | Vue.config.devtools = false;
7 |
8 | hljs.registerLanguage('php', require('./libs/languages/php'));
9 |
10 | var indent = " ";
11 | var fakerText="";
12 | fakerText += indent + "\/**" + "\n";
13 | fakerText += indent + " * @var Faker\Generator" + "\n";
14 | fakerText += indent + " *\/" + "\n";
15 | fakerText += indent + "protected $faker;" + "\n";
16 | fakerText += indent + "" + "\n";
17 | fakerText += indent + "\/**" + "\n";
18 | fakerText += indent + " * Setup faker" + "\n";
19 | fakerText += indent + " *\/" + "\n";
20 | fakerText += indent + "public function setUp()" + "\n";
21 | fakerText += indent + "{" + "\n";
22 | fakerText += indent + " parent::setUp();" + "\n";
23 | fakerText += indent + " $this->faker = \\Faker\\Factory::create();" + "\n";
24 | fakerText += indent + "}";
25 |
26 | var App = new Vue({
27 | el: "body",
28 |
29 | data: {
30 | renaming: false,
31 | editSettings: false,
32 | linebreak: "\n",
33 | indent: " ",
34 | duskIndent: " ",
35 | steps: [],
36 | message: '',
37 | namespace: '',
38 | className: 'ExampleTest',
39 | testName: helper.ucfirst(faker.company.catchPhraseNoun().replace('-','').replace(' ','')) + 'Is' + helper.ucfirst(faker.commerce.productAdjective().replace('-','')),
40 | recording: false,
41 |
42 | withoutMiddleware: false,
43 | dbMigrations: false,
44 | dbTransactions: false,
45 | useDuskTests: false
46 | },
47 |
48 | watch: {
49 | 'namespace': function() {
50 | this.updateCode();
51 | },
52 |
53 | 'className': function() {
54 | this.updateCode();
55 | },
56 |
57 | 'testName': function() {
58 | this.updateCode();
59 | },
60 |
61 | 'withoutMiddleware': function() {
62 | this.updateCode();
63 | },
64 |
65 | 'dbMigrations': function() {
66 | this.updateCode();
67 | },
68 |
69 | 'useDuskTests': function() {
70 | this.updateCode();
71 | },
72 |
73 | 'dbTransactions': function() {
74 | this.updateCode();
75 | },
76 |
77 | 'steps': function(val, oldVal) {
78 | this.updateCode();
79 | },
80 |
81 | 'recording': function(val, oldVal) {
82 | _postMessage({
83 | 'method': 'recording',
84 | 'value': val
85 | });
86 | }
87 | },
88 |
89 | computed: {
90 |
91 | duskSteps: function() {
92 | /**
93 | * We need to modify some arguments in order to work with Dusk tests.
94 | */
95 | var steps = this.steps.slice();
96 |
97 | steps = steps.map(function (step) {
98 | step = Object.assign({}, step);
99 | step.args = step.args.slice();
100 |
101 | if (step.method === 'type') {
102 | step.args = step.args.reverse();
103 | }
104 | if (step.method === 'seePageIs') {
105 | step.method = 'assertPathIs';
106 | }
107 | if (step.method === 'see') {
108 | step.method = 'assertSee';
109 | }
110 | return step;
111 | });
112 |
113 | return steps;
114 | },
115 |
116 | regularSteps: function() {
117 | /**
118 | * We need to remove some methods that are not compatible without Dusk.
119 | */
120 | var steps = this.steps.slice();
121 |
122 | steps = steps.map(function (step) {
123 | step = Object.assign({}, step);
124 | step.args = step.args.slice();
125 | return step;
126 | });
127 |
128 | steps = steps.filter(function (step) {
129 | return step.method !== 'waitForText' && step.method !== 'clickLink';
130 | });
131 |
132 | return steps;
133 | },
134 |
135 | useStatements: function() {
136 | var statements = '';
137 | if (this.dbTransactions) {
138 | statements += 'use Illuminate\\Foundation\\Testing\\DatabaseTransactions;'+"\n";
139 | }
140 | if (this.dbMigrations) {
141 | statements += 'use Illuminate\\Foundation\\Testing\\DatabaseMigrations;'+"\n";
142 | }
143 | if (this.withoutMiddleware) {
144 | statements += 'use Illuminate\\Foundation\\Testing\\WithoutMiddleware;'+"\n";
145 | }
146 | if (this.useDuskTests) {
147 | statements += 'use Tests\\DuskTestCase;'+"\n";
148 | statements += 'use Laravel\\Dusk\\Browser;'+"\n";
149 | }
150 | return statements;
151 | },
152 |
153 | enabledTraits: function() {
154 | var traits = '';
155 | if (this.dbTransactions) {
156 | traits += ' '+'use DatabaseTransactions;'+"\n";
157 | }
158 | if (this.dbMigrations) {
159 | traits += ' '+'use DatabaseMigrations;'+"\n";
160 | }
161 | if (this.withoutMiddleware) {
162 | traits += ' '+'use WithoutMiddleware;'+"\n";
163 | }
164 | return traits;
165 | },
166 |
167 | hasFaker: function() {
168 | var hasFaker = false;
169 | this.steps.forEach(function(step){
170 | if (step.faker) {
171 | hasFaker = true;
172 | }
173 | });
174 | return hasFaker;
175 | }
176 | },
177 |
178 | filters: {
179 | implode: function(val, faker) {
180 | var result = '';
181 | if (val) {
182 | val.forEach(function(attribute, key){
183 | if (key === 0 && faker === true) {
184 | result += ""+attribute+", ";
185 | } else {
186 | result += "'"+helper.addslashes(attribute)+"', ";
187 | }
188 | });
189 | return result.slice(0,-2);
190 | }
191 | return result;
192 | }
193 | },
194 |
195 | methods: {
196 | clear: function() {
197 | _postMessage({
198 | 'method': 'clear'
199 | });
200 | },
201 |
202 | undo: function() {
203 | _postMessage({
204 | 'method': 'undo'
205 | });
206 | },
207 |
208 | rename: function() {
209 | this.renaming = ! this.renaming;
210 | this.editSettings = false;
211 | },
212 |
213 | toggleEditSettings: function() {
214 | this.editSettings = ! this.editSettings;
215 | this.renaming = false;
216 | },
217 |
218 | copyTest: function() {
219 | var self = this;
220 | var range = document.createRange();
221 | var selection = window.getSelection();
222 | range.selectNodeContents(document.getElementById('testcode'));
223 | selection.removeAllRanges();
224 | selection.addRange(range);
225 |
226 | window.document.execCommand('Copy');
227 |
228 | this.message = 'Test successfully copied to clipboard.';
229 |
230 | setTimeout(function(){
231 | self.message = '';
232 | }, 1500);
233 | },
234 |
235 | updateCode: function() {
236 | var self = this;
237 | $('#testcode').html(hljs.highlightAuto(
238 | $('#steps')
239 | .text()
240 | .replace('%TESTNAME%', self.testName)
241 | .replace('%CLASSNAME%', self.className)
242 | .replace('%NAMESPACE%', (self.namespace !== '') ? 'namespace ' + self.namespace + ';' + "\n" : '' )
243 | .replace('%CLASS_USE_STATEMENTS%', self.useStatements)
244 | .replace('%ENABLED_TRAITS%', self.enabledTraits)
245 | .replace('%DUSK%', self.useDuskTests ? 'Dusk' : '')
246 | .replace('%FAKER%', this.hasFaker ? fakerText : '' )
247 | ).value
248 | );
249 | }
250 |
251 | }
252 |
253 | });
254 |
255 | window.setSteps = function(message) {
256 | App.steps = message.steps;
257 | };
258 |
259 | window.setPathname = function(pathname) {
260 | var className = '';
261 |
262 | pathname.split('/').map(function(part){
263 | className += helper.ucfirst(part).replace(/[^\w]/gi, '');
264 | });
265 | App.className = className + 'Test';
266 | };
267 |
--------------------------------------------------------------------------------
/code/js/content.js:
--------------------------------------------------------------------------------
1 | var $ = require('jquery'),
2 | faker = require('faker/locale/en_US'),
3 | Vue = require('vue');
4 | Vue.config.devtools = false;
5 |
6 | var App = new Vue({
7 |
8 | data: {
9 | steps: [],
10 | recording: false
11 | },
12 |
13 | created: function() {
14 | var self = this;
15 |
16 | chrome.storage.local.get(null,function(items) {
17 | self.recording = items.recording || false;
18 |
19 | if (items.steps) {
20 | self.steps = items.steps;
21 | }
22 | self.initializeEvents();
23 | });
24 | },
25 |
26 | methods: {
27 | initializeEvents: function() {
28 | var self = this;
29 |
30 | if (self.recording === true) {
31 | if (this.steps.length === 0 || this.steps[this.steps.length-1].method !== 'press') {
32 | this.steps.push({
33 | 'method': 'visit',
34 | 'args': [window.location.pathname]
35 | });
36 | } else if (this.steps[this.steps.length-1].method === 'press') {
37 | this.steps.push({
38 | 'method': 'seePageIs',
39 | 'args': [window.location.pathname]
40 | });
41 | }
42 | }
43 |
44 | $(document).on('change', 'textarea, input[type!="checkbox"][type!="file"][type!="submit"]', function(){
45 | if (self.recording === true) {
46 | var name = $(this).attr("name"),
47 | value = $(this).val();
48 | self.steps.push({
49 | 'method': 'type',
50 | 'args': [value, name]
51 | });
52 | }
53 | });
54 |
55 | $(document).on('change', 'input[type="file"]', function(){
56 | if (self.recording === true) {
57 | var name = $(this).attr("name"),
58 | value = 'absolutePathToFile';
59 | self.steps.push({
60 | 'method': 'attach',
61 | 'args': [value, name]
62 | });
63 | }
64 | });
65 |
66 | $(document).on('change', 'input[type="checkbox"]', function(){
67 | if (self.recording === true) {
68 | var name = $(this).attr("name");
69 | if (this.checked) {
70 | self.steps.push({
71 | 'method': 'check',
72 | 'args': [name]
73 | });
74 | } else {
75 | self.steps.push({
76 | 'method': 'uncheck',
77 | 'args': [name]
78 | });
79 | }
80 | }
81 | });
82 |
83 | $(document).on('click', 'input[type="submit"],button', function(){
84 | if (self.recording === true) {
85 | var name = $(this).attr("name") || $(this).text().trim();
86 | if (name === '') {
87 | name = $(this).val();
88 | }
89 | self.steps.push({
90 | 'method': 'press',
91 | 'args': [name]
92 | });
93 | }
94 | });
95 |
96 | $(document).on('click', 'a', function(){
97 | if (self.recording === true) {
98 | var linkText = $(this).text().trim();
99 | if (linkText !== '') {
100 | self.steps.push({
101 | 'method': 'clickLink',
102 | 'args': [linkText]
103 | });
104 | }
105 | }
106 | });
107 |
108 | $(document).on('change', 'select', function(){
109 | if (self.recording === true) {
110 | var name = $(this).attr("name"),
111 | value = $(this).val();
112 | self.steps.push({
113 | 'method': 'select',
114 | 'args': [value, name]
115 | });
116 | }
117 | });
118 | }
119 | },
120 |
121 | watch: {
122 | 'steps': function(val) {
123 | var self = this;
124 | chrome.storage.local.set({'steps': val, 'preserveSteps': self.preserveSteps});
125 | chrome.extension.sendMessage({
126 | 'steps' : val
127 | });
128 | }
129 | },
130 |
131 | });
132 |
133 |
134 | var clickedEl = null;
135 |
136 | document.addEventListener("mousedown", function(event){
137 | if(event.button === 2) {
138 | clickedEl = event.target;
139 | }
140 | }, true);
141 |
142 | chrome.extension.onRequest.addListener(function(request) {
143 |
144 | var method = request.method || false;
145 | if(method === "seeText") {
146 | App.steps.push({
147 | 'method': 'see',
148 | 'args': [request.text]
149 | });
150 | }
151 | if(method === "waitForText") {
152 | App.steps.push({
153 | 'method': 'waitForText',
154 | 'args': [request.text]
155 | });
156 | }
157 | if(method === "press") {
158 | var name = $(clickedEl).attr("name") || $(clickedEl).text().trim();
159 | if (name === '') {
160 | name = $(clickedEl).val();
161 | }
162 | App.steps.push({
163 | 'method': 'press',
164 | 'args': [name]
165 | });
166 | }
167 | if(method === "visit") {
168 | App.steps.push({
169 | 'method': 'visit',
170 | 'args': [window.location.pathname]
171 | });
172 | }
173 | if(method === "seePageIs") {
174 | App.steps.push({
175 | 'method': 'seePageIs',
176 | 'args': [window.location.pathname]
177 | });
178 | }
179 | if(method === "recording") {
180 | App.recording = request.value;
181 | chrome.storage.local.set({'steps': App.steps, 'recording': App.recording});
182 | if (App.recording === true && App.steps.length === 0) {
183 | App.steps.push({
184 | 'method': 'visit',
185 | 'args': [window.location.pathname]
186 | });
187 | }
188 | }
189 | if(method === "clear") {
190 | App.recording = request.value;
191 | App.steps = [];
192 | }
193 | if(method === "undo") {
194 | App.steps.pop();
195 | }
196 | if(method === "importFactories") {
197 | var fileChooser = document.createElement("input");
198 | fileChooser.setAttribute("accept", ".php");
199 | fileChooser.type = 'file';
200 |
201 | fileChooser.addEventListener('change', function (evt) {
202 | var f = evt.target.files[0];
203 | if(f) {
204 | var reader = new FileReader();
205 | reader.onload = function(e) {
206 | var contents = e.target.result;
207 | // Lookup factories and factory properties
208 | var factoryRegex = /factory->define\(\s?(.*)\s?,/g;
209 | var propertyRegex = /\s*[\'\"](.*)[\'\"]\s*=>\s*(.*)\s*,/g;
210 | var match, factoryMatch;
211 | if (contents.match(factoryRegex) === null) {
212 | alert("No Laravel factories found.\nPlease select the database/factories/ModelFactory.php file.");
213 | return;
214 | }
215 | var factories = [],
216 | factoryIndex = 0,
217 | properties = [];
218 | var factoryStrings = contents.split(/factory->define\(\s?.*\s?,/);
219 | factoryStrings.shift();
220 | while ((match = factoryRegex.exec(contents)) !== null) {
221 | properties = [];
222 | if (match.index === factoryRegex.lastIndex) {
223 | factoryRegex.lastIndex++;
224 | }
225 | while ((factoryMatch = propertyRegex.exec(factoryStrings[factoryIndex])) !== null) {
226 | if (factoryMatch.index === propertyRegex.lastIndex) {
227 | propertyRegex.lastIndex++;
228 | }
229 | properties.push({
230 | key: factoryMatch[1],
231 | value: factoryMatch[2]
232 | });
233 | }
234 | factoryIndex++;
235 | factories.push({
236 | name: match[1],
237 | properties: properties
238 | });
239 | }
240 | alert("Successfully imported "+factories.length+" factories.");
241 | chrome.extension.sendMessage({
242 | 'factories' : factories
243 | });
244 | };
245 | reader.readAsText(f);
246 | }
247 | });
248 |
249 | fileChooser.click();
250 | }
251 |
252 | if(method === "createFactoryModel") {
253 | App.steps.push({
254 | 'custom': true,
255 | 'action': '$model = factory('+request.model+')->make()'
256 | });
257 | }
258 |
259 | if(method === "fake") {
260 | var fakeData = "";
261 |
262 | switch (request.type) {
263 | case "email":
264 | fakeData = faker.internet.email();
265 | break;
266 | case "name":
267 | fakeData = faker.name.findName();
268 | break;
269 | case "firstname":
270 | fakeData = faker.name.firstName();
271 | break;
272 | case "lastname":
273 | fakeData = faker.name.lastName();
274 | break;
275 | case "word":
276 | fakeData = faker.lorem.words().pop();
277 | break;
278 | case "url":
279 | fakeData = faker.internet.url();
280 | break;
281 | }
282 | $(clickedEl).val(fakeData);
283 |
284 | App.steps.push({
285 | 'method': 'type',
286 | 'faker': true,
287 | 'args': ['$this->faker->'+request.type, $(clickedEl).attr("name")]
288 | });
289 | }
290 |
291 | if(method === "getSteps") {
292 | chrome.extension.sendMessage({
293 | 'steps' : App.steps
294 | });
295 | }
296 | });
297 |
--------------------------------------------------------------------------------
/code/js/libs/highlight.js:
--------------------------------------------------------------------------------
1 | /*
2 | Syntax highlighting with language autodetection.
3 | https://highlightjs.org/
4 | */
5 |
6 | (function(factory) {
7 |
8 | // Find the global object for export to both the browser and web workers.
9 | var globalObject = typeof window == 'object' && window ||
10 | typeof self == 'object' && self;
11 |
12 | // Setup highlight.js for different environments. First is Node.js or
13 | // CommonJS.
14 | if(typeof exports !== 'undefined') {
15 | factory(exports);
16 | } else if(globalObject) {
17 | // Export hljs globally even when using AMD for cases when this script
18 | // is loaded with others that may still expect a global hljs.
19 | globalObject.hljs = factory({});
20 |
21 | // Finally register the global hljs with AMD.
22 | if(typeof define === 'function' && define.amd) {
23 | define([], function() {
24 | return globalObject.hljs;
25 | });
26 | }
27 | }
28 |
29 | }(function(hljs) {
30 |
31 | /* Utility functions */
32 |
33 | function escape(value) {
34 | return value.replace(/&/gm, '&').replace(//gm, '>');
35 | }
36 |
37 | function tag(node) {
38 | return node.nodeName.toLowerCase();
39 | }
40 |
41 | function testRe(re, lexeme) {
42 | var match = re && re.exec(lexeme);
43 | return match && match.index == 0;
44 | }
45 |
46 | function isNotHighlighted(language) {
47 | return (/^(no-?highlight|plain|text)$/i).test(language);
48 | }
49 |
50 | function blockLanguage(block) {
51 | var i, match, length,
52 | classes = block.className + ' ';
53 |
54 | classes += block.parentNode ? block.parentNode.className : '';
55 |
56 | // language-* takes precedence over non-prefixed class names.
57 | match = (/\blang(?:uage)?-([\w-]+)\b/i).exec(classes);
58 | if (match) {
59 | return getLanguage(match[1]) ? match[1] : 'no-highlight';
60 | }
61 |
62 | classes = classes.split(/\s+/);
63 | for (i = 0, length = classes.length; i < length; i++) {
64 | if (getLanguage(classes[i]) || isNotHighlighted(classes[i])) {
65 | return classes[i];
66 | }
67 | }
68 | }
69 |
70 | function inherit(parent, obj) {
71 | var result = {}, key;
72 | for (key in parent)
73 | result[key] = parent[key];
74 | if (obj)
75 | for (key in obj)
76 | result[key] = obj[key];
77 | return result;
78 | }
79 |
80 | /* Stream merging */
81 |
82 | function nodeStream(node) {
83 | var result = [];
84 | (function _nodeStream(node, offset) {
85 | for (var child = node.firstChild; child; child = child.nextSibling) {
86 | if (child.nodeType == 3)
87 | offset += child.nodeValue.length;
88 | else if (child.nodeType == 1) {
89 | result.push({
90 | event: 'start',
91 | offset: offset,
92 | node: child
93 | });
94 | offset = _nodeStream(child, offset);
95 | // Prevent void elements from having an end tag that would actually
96 | // double them in the output. There are more void elements in HTML
97 | // but we list only those realistically expected in code display.
98 | if (!tag(child).match(/br|hr|img|input/)) {
99 | result.push({
100 | event: 'stop',
101 | offset: offset,
102 | node: child
103 | });
104 | }
105 | }
106 | }
107 | return offset;
108 | })(node, 0);
109 | return result;
110 | }
111 |
112 | function mergeStreams(original, highlighted, value) {
113 | var processed = 0;
114 | var result = '';
115 | var nodeStack = [];
116 |
117 | function selectStream() {
118 | if (!original.length || !highlighted.length) {
119 | return original.length ? original : highlighted;
120 | }
121 | if (original[0].offset != highlighted[0].offset) {
122 | return (original[0].offset < highlighted[0].offset) ? original : highlighted;
123 | }
124 |
125 | /*
126 | To avoid starting the stream just before it should stop the order is
127 | ensured that original always starts first and closes last:
128 |
129 | if (event1 == 'start' && event2 == 'start')
130 | return original;
131 | if (event1 == 'start' && event2 == 'stop')
132 | return highlighted;
133 | if (event1 == 'stop' && event2 == 'start')
134 | return original;
135 | if (event1 == 'stop' && event2 == 'stop')
136 | return highlighted;
137 |
138 | ... which is collapsed to:
139 | */
140 | return highlighted[0].event == 'start' ? original : highlighted;
141 | }
142 |
143 | function open(node) {
144 | function attr_str(a) {return ' ' + a.nodeName + '="' + escape(a.value) + '"';}
145 | result += '<' + tag(node) + Array.prototype.map.call(node.attributes, attr_str).join('') + '>';
146 | }
147 |
148 | function close(node) {
149 | result += '' + tag(node) + '>';
150 | }
151 |
152 | function render(event) {
153 | (event.event == 'start' ? open : close)(event.node);
154 | }
155 |
156 | while (original.length || highlighted.length) {
157 | var stream = selectStream();
158 | result += escape(value.substr(processed, stream[0].offset - processed));
159 | processed = stream[0].offset;
160 | if (stream == original) {
161 | /*
162 | On any opening or closing tag of the original markup we first close
163 | the entire highlighted node stack, then render the original tag along
164 | with all the following original tags at the same offset and then
165 | reopen all the tags on the highlighted stack.
166 | */
167 | nodeStack.reverse().forEach(close);
168 | do {
169 | render(stream.splice(0, 1)[0]);
170 | stream = selectStream();
171 | } while (stream == original && stream.length && stream[0].offset == processed);
172 | nodeStack.reverse().forEach(open);
173 | } else {
174 | if (stream[0].event == 'start') {
175 | nodeStack.push(stream[0].node);
176 | } else {
177 | nodeStack.pop();
178 | }
179 | render(stream.splice(0, 1)[0]);
180 | }
181 | }
182 | return result + escape(value.substr(processed));
183 | }
184 |
185 | /* Initialization */
186 |
187 | function compileLanguage(language) {
188 |
189 | function reStr(re) {
190 | return (re && re.source) || re;
191 | }
192 |
193 | function langRe(value, global) {
194 | return new RegExp(
195 | reStr(value),
196 | 'm' + (language.case_insensitive ? 'i' : '') + (global ? 'g' : '')
197 | );
198 | }
199 |
200 | function compileMode(mode, parent) {
201 | if (mode.compiled)
202 | return;
203 | mode.compiled = true;
204 |
205 | mode.keywords = mode.keywords || mode.beginKeywords;
206 | if (mode.keywords) {
207 | var compiled_keywords = {};
208 |
209 | var flatten = function(className, str) {
210 | if (language.case_insensitive) {
211 | str = str.toLowerCase();
212 | }
213 | str.split(' ').forEach(function(kw) {
214 | var pair = kw.split('|');
215 | compiled_keywords[pair[0]] = [className, pair[1] ? Number(pair[1]) : 1];
216 | });
217 | };
218 |
219 | if (typeof mode.keywords == 'string') { // string
220 | flatten('keyword', mode.keywords);
221 | } else {
222 | Object.keys(mode.keywords).forEach(function (className) {
223 | flatten(className, mode.keywords[className]);
224 | });
225 | }
226 | mode.keywords = compiled_keywords;
227 | }
228 | mode.lexemesRe = langRe(mode.lexemes || /\w+/, true);
229 |
230 | if (parent) {
231 | if (mode.beginKeywords) {
232 | mode.begin = '\\b(' + mode.beginKeywords.split(' ').join('|') + ')\\b';
233 | }
234 | if (!mode.begin)
235 | mode.begin = /\B|\b/;
236 | mode.beginRe = langRe(mode.begin);
237 | if (!mode.end && !mode.endsWithParent)
238 | mode.end = /\B|\b/;
239 | if (mode.end)
240 | mode.endRe = langRe(mode.end);
241 | mode.terminator_end = reStr(mode.end) || '';
242 | if (mode.endsWithParent && parent.terminator_end)
243 | mode.terminator_end += (mode.end ? '|' : '') + parent.terminator_end;
244 | }
245 | if (mode.illegal)
246 | mode.illegalRe = langRe(mode.illegal);
247 | if (mode.relevance === undefined)
248 | mode.relevance = 1;
249 | if (!mode.contains) {
250 | mode.contains = [];
251 | }
252 | var expanded_contains = [];
253 | mode.contains.forEach(function(c) {
254 | if (c.variants) {
255 | c.variants.forEach(function(v) {expanded_contains.push(inherit(c, v));});
256 | } else {
257 | expanded_contains.push(c == 'self' ? mode : c);
258 | }
259 | });
260 | mode.contains = expanded_contains;
261 | mode.contains.forEach(function(c) {compileMode(c, mode);});
262 |
263 | if (mode.starts) {
264 | compileMode(mode.starts, parent);
265 | }
266 |
267 | var terminators =
268 | mode.contains.map(function(c) {
269 | return c.beginKeywords ? '\\.?(' + c.begin + ')\\.?' : c.begin;
270 | })
271 | .concat([mode.terminator_end, mode.illegal])
272 | .map(reStr)
273 | .filter(Boolean);
274 | mode.terminators = terminators.length ? langRe(terminators.join('|'), true) : {exec: function(/*s*/) {return null;}};
275 | }
276 |
277 | compileMode(language);
278 | }
279 |
280 | /*
281 | Core highlighting function. Accepts a language name, or an alias, and a
282 | string with the code to highlight. Returns an object with the following
283 | properties:
284 |
285 | - relevance (int)
286 | - value (an HTML string with highlighting markup)
287 |
288 | */
289 | function highlight(name, value, ignore_illegals, continuation) {
290 |
291 | function subMode(lexeme, mode) {
292 | for (var i = 0; i < mode.contains.length; i++) {
293 | if (testRe(mode.contains[i].beginRe, lexeme)) {
294 | return mode.contains[i];
295 | }
296 | }
297 | }
298 |
299 | function endOfMode(mode, lexeme) {
300 | if (testRe(mode.endRe, lexeme)) {
301 | while (mode.endsParent && mode.parent) {
302 | mode = mode.parent;
303 | }
304 | return mode;
305 | }
306 | if (mode.endsWithParent) {
307 | return endOfMode(mode.parent, lexeme);
308 | }
309 | }
310 |
311 | function isIllegal(lexeme, mode) {
312 | return !ignore_illegals && testRe(mode.illegalRe, lexeme);
313 | }
314 |
315 | function keywordMatch(mode, match) {
316 | var match_str = language.case_insensitive ? match[0].toLowerCase() : match[0];
317 | return mode.keywords.hasOwnProperty(match_str) && mode.keywords[match_str];
318 | }
319 |
320 | function buildSpan(classname, insideSpan, leaveOpen, noPrefix) {
321 | var classPrefix = noPrefix ? '' : options.classPrefix,
322 | openSpan = '';
326 |
327 | return openSpan + insideSpan + closeSpan;
328 | }
329 |
330 | function processKeywords() {
331 | if (!top.keywords)
332 | return escape(mode_buffer);
333 | var result = '';
334 | var last_index = 0;
335 | top.lexemesRe.lastIndex = 0;
336 | var match = top.lexemesRe.exec(mode_buffer);
337 | while (match) {
338 | result += escape(mode_buffer.substr(last_index, match.index - last_index));
339 | var keyword_match = keywordMatch(top, match);
340 | if (keyword_match) {
341 | relevance += keyword_match[1];
342 | result += buildSpan(keyword_match[0], escape(match[0]));
343 | } else {
344 | result += escape(match[0]);
345 | }
346 | last_index = top.lexemesRe.lastIndex;
347 | match = top.lexemesRe.exec(mode_buffer);
348 | }
349 | return result + escape(mode_buffer.substr(last_index));
350 | }
351 |
352 | function processSubLanguage() {
353 | var explicit = typeof top.subLanguage == 'string';
354 | if (explicit && !languages[top.subLanguage]) {
355 | return escape(mode_buffer);
356 | }
357 |
358 | var result = explicit ?
359 | highlight(top.subLanguage, mode_buffer, true, continuations[top.subLanguage]) :
360 | highlightAuto(mode_buffer, top.subLanguage.length ? top.subLanguage : undefined);
361 |
362 | // Counting embedded language score towards the host language may be disabled
363 | // with zeroing the containing mode relevance. Usecase in point is Markdown that
364 | // allows XML everywhere and makes every XML snippet to have a much larger Markdown
365 | // score.
366 | if (top.relevance > 0) {
367 | relevance += result.relevance;
368 | }
369 | if (explicit) {
370 | continuations[top.subLanguage] = result.top;
371 | }
372 | return buildSpan(result.language, result.value, false, true);
373 | }
374 |
375 | function processBuffer() {
376 | result += (top.subLanguage !== undefined ? processSubLanguage() : processKeywords());
377 | mode_buffer = '';
378 | }
379 |
380 | function startNewMode(mode, lexeme) {
381 | result += mode.className? buildSpan(mode.className, '', true): '';
382 | top = Object.create(mode, {parent: {value: top}});
383 | }
384 |
385 | function processLexeme(buffer, lexeme) {
386 |
387 | mode_buffer += buffer;
388 |
389 | if (lexeme === undefined) {
390 | processBuffer();
391 | return 0;
392 | }
393 |
394 | var new_mode = subMode(lexeme, top);
395 | if (new_mode) {
396 | if (new_mode.skip) {
397 | mode_buffer += lexeme;
398 | } else {
399 | if (new_mode.excludeBegin) {
400 | mode_buffer += lexeme;
401 | }
402 | processBuffer();
403 | if (!new_mode.returnBegin && !new_mode.excludeBegin) {
404 | mode_buffer = lexeme;
405 | }
406 | }
407 | startNewMode(new_mode, lexeme);
408 | return new_mode.returnBegin ? 0 : lexeme.length;
409 | }
410 |
411 | var end_mode = endOfMode(top, lexeme);
412 | if (end_mode) {
413 | var origin = top;
414 | if (origin.skip) {
415 | mode_buffer += lexeme;
416 | } else {
417 | if (!(origin.returnEnd || origin.excludeEnd)) {
418 | mode_buffer += lexeme;
419 | }
420 | processBuffer();
421 | if (origin.excludeEnd) {
422 | mode_buffer = lexeme;
423 | }
424 | }
425 | do {
426 | if (top.className) {
427 | result += '';
428 | }
429 | if (!top.skip) {
430 | relevance += top.relevance;
431 | }
432 | top = top.parent;
433 | } while (top != end_mode.parent);
434 | if (end_mode.starts) {
435 | startNewMode(end_mode.starts, '');
436 | }
437 | return origin.returnEnd ? 0 : lexeme.length;
438 | }
439 |
440 | if (isIllegal(lexeme, top))
441 | throw new Error('Illegal lexeme "' + lexeme + '" for mode "' + (top.className || '') + '"');
442 |
443 | /*
444 | Parser should not reach this point as all types of lexemes should be caught
445 | earlier, but if it does due to some bug make sure it advances at least one
446 | character forward to prevent infinite looping.
447 | */
448 | mode_buffer += lexeme;
449 | return lexeme.length || 1;
450 | }
451 |
452 | var language = getLanguage(name);
453 | if (!language) {
454 | throw new Error('Unknown language: "' + name + '"');
455 | }
456 |
457 | compileLanguage(language);
458 | var top = continuation || language;
459 | var continuations = {}; // keep continuations for sub-languages
460 | var result = '', current;
461 | for(current = top; current != language; current = current.parent) {
462 | if (current.className) {
463 | result = buildSpan(current.className, '', true) + result;
464 | }
465 | }
466 | var mode_buffer = '';
467 | var relevance = 0;
468 | try {
469 | var match, count, index = 0;
470 | while (true) {
471 | top.terminators.lastIndex = index;
472 | match = top.terminators.exec(value);
473 | if (!match)
474 | break;
475 | count = processLexeme(value.substr(index, match.index - index), match[0]);
476 | index = match.index + count;
477 | }
478 | processLexeme(value.substr(index));
479 | for(current = top; current.parent; current = current.parent) { // close dangling modes
480 | if (current.className) {
481 | result += '';
482 | }
483 | }
484 | return {
485 | relevance: relevance,
486 | value: result,
487 | language: name,
488 | top: top
489 | };
490 | } catch (e) {
491 | if (e.message.indexOf('Illegal') != -1) {
492 | return {
493 | relevance: 0,
494 | value: escape(value)
495 | };
496 | } else {
497 | throw e;
498 | }
499 | }
500 | }
501 |
502 | /*
503 | Highlighting with language detection. Accepts a string with the code to
504 | highlight. Returns an object with the following properties:
505 |
506 | - language (detected language)
507 | - relevance (int)
508 | - value (an HTML string with highlighting markup)
509 | - second_best (object with the same structure for second-best heuristically
510 | detected language, may be absent)
511 |
512 | */
513 | function highlightAuto(text, languageSubset) {
514 | languageSubset = languageSubset || options.languages || Object.keys(languages);
515 | var result = {
516 | relevance: 0,
517 | value: escape(text)
518 | };
519 | var second_best = result;
520 | languageSubset.filter(getLanguage).forEach(function(name) {
521 | var current = highlight(name, text, false);
522 | current.language = name;
523 | if (current.relevance > second_best.relevance) {
524 | second_best = current;
525 | }
526 | if (current.relevance > result.relevance) {
527 | second_best = result;
528 | result = current;
529 | }
530 | });
531 | if (second_best.language) {
532 | result.second_best = second_best;
533 | }
534 | return result;
535 | }
536 |
537 | /*
538 | Post-processing of the highlighted markup:
539 |
540 | - replace TABs with something more useful
541 | - replace real line-breaks with '
' for non-pre containers
542 |
543 | */
544 | function fixMarkup(value) {
545 | if (options.tabReplace) {
546 | value = value.replace(/^((<[^>]+>|\t)+)/gm, function(match, p1 /*..., offset, s*/) {
547 | return p1.replace(/\t/g, options.tabReplace);
548 | });
549 | }
550 | if (options.useBR) {
551 | value = value.replace(/\n/g, '
');
552 | }
553 | return value;
554 | }
555 |
556 | function buildClassName(prevClassName, currentLang, resultLang) {
557 | var language = currentLang ? aliases[currentLang] : resultLang,
558 | result = [prevClassName.trim()];
559 |
560 | if (!prevClassName.match(/\bhljs\b/)) {
561 | result.push('hljs');
562 | }
563 |
564 | if (prevClassName.indexOf(language) === -1) {
565 | result.push(language);
566 | }
567 |
568 | return result.join(' ').trim();
569 | }
570 |
571 | /*
572 | Applies highlighting to a DOM node containing code. Accepts a DOM node and
573 | two optional parameters for fixMarkup.
574 | */
575 | function highlightBlock(block) {
576 | var language = blockLanguage(block);
577 | if (isNotHighlighted(language))
578 | return;
579 |
580 | var node;
581 | if (options.useBR) {
582 | node = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
583 | node.innerHTML = block.innerHTML.replace(/\n/g, '').replace(/
/g, '\n');
584 | } else {
585 | node = block;
586 | }
587 | var text = node.textContent;
588 | var result = language ? highlight(language, text, true) : highlightAuto(text);
589 |
590 | var originalStream = nodeStream(node);
591 | if (originalStream.length) {
592 | var resultNode = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
593 | resultNode.innerHTML = result.value;
594 | result.value = mergeStreams(originalStream, nodeStream(resultNode), text);
595 | }
596 | result.value = fixMarkup(result.value);
597 |
598 | block.innerHTML = result.value;
599 | block.className = buildClassName(block.className, language, result.language);
600 | block.result = {
601 | language: result.language,
602 | re: result.relevance
603 | };
604 | if (result.second_best) {
605 | block.second_best = {
606 | language: result.second_best.language,
607 | re: result.second_best.relevance
608 | };
609 | }
610 | }
611 |
612 | var options = {
613 | classPrefix: 'hljs-',
614 | tabReplace: null,
615 | useBR: false,
616 | languages: undefined
617 | };
618 |
619 | /*
620 | Updates highlight.js global options with values passed in the form of an object.
621 | */
622 | function configure(user_options) {
623 | options = inherit(options, user_options);
624 | }
625 |
626 | /*
627 | Applies highlighting to all ..
blocks on a page.
628 | */
629 | function initHighlighting() {
630 | if (initHighlighting.called)
631 | return;
632 | initHighlighting.called = true;
633 |
634 | var blocks = document.querySelectorAll('pre code');
635 | Array.prototype.forEach.call(blocks, highlightBlock);
636 | }
637 |
638 | /*
639 | Attaches highlighting to the page load event.
640 | */
641 | function initHighlightingOnLoad() {
642 | addEventListener('DOMContentLoaded', initHighlighting, false);
643 | addEventListener('load', initHighlighting, false);
644 | }
645 |
646 | var languages = {};
647 | var aliases = {};
648 |
649 | function registerLanguage(name, language) {
650 | var lang = languages[name] = language(hljs);
651 | if (lang.aliases) {
652 | lang.aliases.forEach(function(alias) {aliases[alias] = name;});
653 | }
654 | }
655 |
656 | function listLanguages() {
657 | return Object.keys(languages);
658 | }
659 |
660 | function getLanguage(name) {
661 | name = (name || '').toLowerCase();
662 | return languages[name] || languages[aliases[name]];
663 | }
664 |
665 | /* Interface definition */
666 |
667 | hljs.highlight = highlight;
668 | hljs.highlightAuto = highlightAuto;
669 | hljs.fixMarkup = fixMarkup;
670 | hljs.highlightBlock = highlightBlock;
671 | hljs.configure = configure;
672 | hljs.initHighlighting = initHighlighting;
673 | hljs.initHighlightingOnLoad = initHighlightingOnLoad;
674 | hljs.registerLanguage = registerLanguage;
675 | hljs.listLanguages = listLanguages;
676 | hljs.getLanguage = getLanguage;
677 | hljs.inherit = inherit;
678 |
679 | // Common regexps
680 | hljs.IDENT_RE = '[a-zA-Z]\\w*';
681 | hljs.UNDERSCORE_IDENT_RE = '[a-zA-Z_]\\w*';
682 | hljs.NUMBER_RE = '\\b\\d+(\\.\\d+)?';
683 | hljs.C_NUMBER_RE = '(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)'; // 0x..., 0..., decimal, float
684 | hljs.BINARY_NUMBER_RE = '\\b(0b[01]+)'; // 0b...
685 | hljs.RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~';
686 |
687 | // Common modes
688 | hljs.BACKSLASH_ESCAPE = {
689 | begin: '\\\\[\\s\\S]', relevance: 0
690 | };
691 | hljs.APOS_STRING_MODE = {
692 | className: 'string',
693 | begin: '\'', end: '\'',
694 | illegal: '\\n',
695 | contains: [hljs.BACKSLASH_ESCAPE]
696 | };
697 | hljs.QUOTE_STRING_MODE = {
698 | className: 'string',
699 | begin: '"', end: '"',
700 | illegal: '\\n',
701 | contains: [hljs.BACKSLASH_ESCAPE]
702 | };
703 | hljs.PHRASAL_WORDS_MODE = {
704 | begin: /\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/
705 | };
706 | hljs.COMMENT = function (begin, end, inherits) {
707 | var mode = hljs.inherit(
708 | {
709 | className: 'comment',
710 | begin: begin, end: end,
711 | contains: []
712 | },
713 | inherits || {}
714 | );
715 | mode.contains.push(hljs.PHRASAL_WORDS_MODE);
716 | mode.contains.push({
717 | className: 'doctag',
718 | begin: "(?:TODO|FIXME|NOTE|BUG|XXX):",
719 | relevance: 0
720 | });
721 | return mode;
722 | };
723 | hljs.C_LINE_COMMENT_MODE = hljs.COMMENT('//', '$');
724 | hljs.C_BLOCK_COMMENT_MODE = hljs.COMMENT('/\\*', '\\*/');
725 | hljs.HASH_COMMENT_MODE = hljs.COMMENT('#', '$');
726 | hljs.NUMBER_MODE = {
727 | className: 'number',
728 | begin: hljs.NUMBER_RE,
729 | relevance: 0
730 | };
731 | hljs.C_NUMBER_MODE = {
732 | className: 'number',
733 | begin: hljs.C_NUMBER_RE,
734 | relevance: 0
735 | };
736 | hljs.BINARY_NUMBER_MODE = {
737 | className: 'number',
738 | begin: hljs.BINARY_NUMBER_RE,
739 | relevance: 0
740 | };
741 | hljs.CSS_NUMBER_MODE = {
742 | className: 'number',
743 | begin: hljs.NUMBER_RE + '(' +
744 | '%|em|ex|ch|rem' +
745 | '|vw|vh|vmin|vmax' +
746 | '|cm|mm|in|pt|pc|px' +
747 | '|deg|grad|rad|turn' +
748 | '|s|ms' +
749 | '|Hz|kHz' +
750 | '|dpi|dpcm|dppx' +
751 | ')?',
752 | relevance: 0
753 | };
754 | hljs.REGEXP_MODE = {
755 | className: 'regexp',
756 | begin: /\//, end: /\/[gimuy]*/,
757 | illegal: /\n/,
758 | contains: [
759 | hljs.BACKSLASH_ESCAPE,
760 | {
761 | begin: /\[/, end: /\]/,
762 | relevance: 0,
763 | contains: [hljs.BACKSLASH_ESCAPE]
764 | }
765 | ]
766 | };
767 | hljs.TITLE_MODE = {
768 | className: 'title',
769 | begin: hljs.IDENT_RE,
770 | relevance: 0
771 | };
772 | hljs.UNDERSCORE_TITLE_MODE = {
773 | className: 'title',
774 | begin: hljs.UNDERSCORE_IDENT_RE,
775 | relevance: 0
776 | };
777 | hljs.METHOD_GUARD = {
778 | // excludes method names from keyword processing
779 | begin: '\\.\\s*' + hljs.UNDERSCORE_IDENT_RE,
780 | relevance: 0
781 | };
782 |
783 | return hljs;
784 | }));
785 |
--------------------------------------------------------------------------------