├── .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 | 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 += ''; 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 | --------------------------------------------------------------------------------