├── .gitignore ├── .prettierignore ├── github-icon.png ├── .prettierrc ├── src ├── modifiers │ ├── hideLineNumbers.ts │ ├── hideFooter.ts │ ├── line.ts │ ├── caption.ts │ └── highlightLine.ts ├── utils │ └── getLineNumbers.ts └── index.ts ├── .babelrc.js ├── webpack.config.js ├── tests ├── hideLineNumbers.test.js ├── caption.test.js ├── hideFooter.test.js ├── highlighLine.test.js ├── line.test.js └── index.test.js ├── LICENSE ├── package.json ├── README.md ├── tsconfig.json ├── index.html └── dist └── gist-embed.min.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/gist-embed.min.js 2 | -------------------------------------------------------------------------------- /github-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bvanderhoof/gist-embed/HEAD/github-icon.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true, 4 | "semi": true, 5 | "bracketSpacing": false 6 | } 7 | -------------------------------------------------------------------------------- /src/modifiers/hideLineNumbers.ts: -------------------------------------------------------------------------------- 1 | function hideLineNumbers(element: HTMLElement) { 2 | element.querySelectorAll('.js-line-number').forEach(node => { 3 | if (node.parentNode != null) { 4 | node.parentNode.removeChild(node); 5 | } 6 | }); 7 | } 8 | 9 | export default hideLineNumbers; 10 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = api => { 2 | const isTest = api.env('test'); 3 | api.cache(true); 4 | return { 5 | presets: [ 6 | '@babel/preset-typescript', 7 | [ 8 | '@babel/preset-env', 9 | { 10 | targets: { 11 | browsers: ['last 2 versions'], 12 | }, 13 | modules: isTest ? 'commonjs' : false, 14 | useBuiltIns: 'usage', 15 | }, 16 | ], 17 | ], 18 | plugins: ['babel-plugin-rewire'], 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const webpackMerge = require('webpack-merge'); 3 | const modeConfig = mode => require(`./build-utils/webpack.${mode}.js`)(mode); 4 | 5 | module.exports = ({mode} = {mode: 'production'}) => { 6 | return webpackMerge( 7 | { 8 | entry: './src/index.ts', 9 | mode, 10 | plugins: [new webpack.ProgressPlugin()], 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.(ts)$/, 15 | loader: 'babel-loader', 16 | }, 17 | ], 18 | }, 19 | resolve: {extensions: ['.js', '.ts', '.json']}, 20 | }, 21 | modeConfig(mode), 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /tests/hideLineNumbers.test.js: -------------------------------------------------------------------------------- 1 | import hideLineNumbers from '../src/modifiers/hideLineNumbers.ts'; 2 | 3 | function generateMockElement() { 4 | const element = document.createElement('code'); 5 | const lineNumberDIV = document.createElement('div'); 6 | lineNumberDIV.classList.add('js-line-number'); 7 | element.appendChild(lineNumberDIV); 8 | document.body.appendChild(element); 9 | return element; 10 | } 11 | 12 | test('hideLineNumbers', () => { 13 | const element = generateMockElement(); 14 | expect(element.children[0].classList.contains('js-line-number')).toEqual( 15 | true, 16 | ); 17 | hideLineNumbers(element); 18 | expect(element.children.length).toEqual(0); 19 | }); 20 | -------------------------------------------------------------------------------- /src/modifiers/hideFooter.ts: -------------------------------------------------------------------------------- 1 | function hideFooter(element: HTMLElement) { 2 | element.querySelectorAll('.gist-meta').forEach((node: HTMLElement) => { 3 | if (node.parentNode != null) { 4 | node.parentNode.removeChild(node); 5 | } 6 | }); 7 | 8 | // Get rid of the collapsed border from missing footer 9 | element.querySelectorAll('.gist-data').forEach((node: HTMLElement) => { 10 | if (node != null) { 11 | node.style.borderBottom = '0px'; 12 | } 13 | }); 14 | element.querySelectorAll('.gist-file').forEach((node: HTMLElement) => { 15 | if (node != null) { 16 | node.style.borderBottom = '1px solid #dddddd'; 17 | } 18 | }); 19 | } 20 | 21 | export default hideFooter; 22 | -------------------------------------------------------------------------------- /src/modifiers/line.ts: -------------------------------------------------------------------------------- 1 | import getLineNumbers from '../utils/getLineNumbers'; 2 | 3 | function line(element: HTMLElement, lineRangeString: string) { 4 | const fileLineEls = element.querySelectorAll('.js-file-line'); 5 | const lineNumbers = getLineNumbers(lineRangeString, fileLineEls.length); 6 | // find all trs containing code lines that don't exist in the line param 7 | fileLineEls.forEach((lineElement: HTMLElement, index) => { 8 | // If the line number does not exist in the lines we want to show list, remove it 9 | if ( 10 | !lineNumbers.includes(index + 1) && 11 | lineElement.parentNode != null && 12 | lineElement.parentNode.parentNode != null 13 | ) { 14 | lineElement.parentNode.parentNode.removeChild(lineElement.parentNode); 15 | } 16 | }); 17 | } 18 | 19 | export default line; 20 | -------------------------------------------------------------------------------- /tests/caption.test.js: -------------------------------------------------------------------------------- 1 | import caption from '../src/modifiers/caption.ts'; 2 | 3 | function generateMockElement() { 4 | const element = document.createElement('code'); 5 | const table = document.createElement('table'); 6 | const tbody = document.createElement('tbody'); 7 | const tr = document.createElement('tr'); 8 | tbody.appendChild(tr); 9 | table.appendChild(tbody); 10 | element.appendChild(table); 11 | 12 | document.body.appendChild(element); 13 | return element; 14 | } 15 | 16 | test('caption', () => { 17 | const element = generateMockElement(); 18 | const captionValue = 'This is a test caption'; 19 | caption(element, captionValue); 20 | expect(element.querySelectorAll('table tbody tr').length).toEqual(2); 21 | expect(element.querySelectorAll('table tbody tr td')[1].innerHTML).toEqual( 22 | captionValue, 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /src/modifiers/caption.ts: -------------------------------------------------------------------------------- 1 | function caption(element: HTMLElement, captionValue: string) { 2 | element.querySelectorAll('table tbody').forEach((node: HTMLElement) => { 3 | const row = document.createElement('tr'); 4 | const captionTD = document.createElement('td'); 5 | captionTD.style.padding = '10px !important'; 6 | captionTD.style.borderBottom = '10px solid white'; 7 | captionTD.style.backgroundColor = '#f9f9f9'; 8 | captionTD.style.fontWeight = 'bold'; 9 | captionTD.innerHTML = captionValue; 10 | 11 | const spacerTD = document.createElement('td'); 12 | spacerTD.style.backgroundColor = '#f9f9f9'; 13 | spacerTD.style.borderBottom = '10px solid white'; 14 | 15 | row.appendChild(spacerTD); 16 | row.appendChild(captionTD); 17 | 18 | // Shift row to the front of it's children 19 | node.prepend(row); 20 | }); 21 | } 22 | 23 | export default caption; 24 | -------------------------------------------------------------------------------- /src/modifiers/highlightLine.ts: -------------------------------------------------------------------------------- 1 | import getLineNumbers from '../utils/getLineNumbers'; 2 | 3 | function highlightLine(element: HTMLElement, lineRangeString: string) { 4 | const fileLineEls = element.querySelectorAll('.js-file-line'); 5 | const highlightLineNumbers = getLineNumbers( 6 | lineRangeString, 7 | fileLineEls.length, 8 | ); 9 | 10 | // we need to set the line-data td to 100% so the highlight expands the whole line 11 | element.querySelectorAll('td.line-data').forEach((el: HTMLElement) => { 12 | el.style.width = '100%'; 13 | }); 14 | 15 | // find all .js-file-line tds (actual code lines) that match the highlightLines and add the highlight class 16 | fileLineEls.forEach((el: HTMLElement, index: number) => { 17 | if (highlightLineNumbers.includes(index + 1)) { 18 | el.style.backgroundColor = 'rgb(255, 255, 204)'; 19 | } 20 | }); 21 | } 22 | 23 | export default highlightLine; 24 | -------------------------------------------------------------------------------- /tests/hideFooter.test.js: -------------------------------------------------------------------------------- 1 | import hideFooter from '../src/modifiers/hideFooter.ts'; 2 | 3 | function generateMockElement() { 4 | const element = document.createElement('code'); 5 | 6 | const footerDIV = document.createElement('div'); 7 | footerDIV.classList.add('gist-meta'); 8 | element.appendChild(footerDIV); 9 | 10 | const dataDIV = document.createElement('div'); 11 | dataDIV.classList.add('gist-data', 'notranslate'); 12 | element.appendChild(dataDIV); 13 | 14 | const fileDIV = document.createElement('div'); 15 | fileDIV.classList.add('gist-file'); 16 | element.appendChild(fileDIV); 17 | 18 | document.body.appendChild(element); 19 | return element; 20 | } 21 | 22 | test('hideFooter', () => { 23 | const element = generateMockElement(); 24 | expect(element.children[0].classList.contains('gist-meta')).toEqual(true); 25 | hideFooter(element); 26 | expect(element.querySelectorAll('.gist-meta').length).toEqual(0); 27 | expect(element.querySelector('.gist-data').style.borderBottom).toEqual('0px'); 28 | expect(element.querySelector('.gist-file').style.borderBottom).toEqual( 29 | '1px solid #dddddd', 30 | ); 31 | }); 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Blair Vanderhoof 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/utils/getLineNumbers.ts: -------------------------------------------------------------------------------- 1 | function getLineNumbers( 2 | lineRangeString: string, 3 | totalLines?: number, 4 | ): Array { 5 | const lineNumbers: number[] = []; 6 | // lineRangeString can be 1,2,3 or 1-4,5 7 | // Dash supports the range, commas are specfic line numbers 8 | lineRangeString.split(',').forEach((line: string) => { 9 | const range = line.split('-'); 10 | const start = parseInt(range[0], 10); 11 | let end = parseInt(range[1], 10); 12 | // If this is a range, push the numbers inclusive in that range 13 | if (range.length === 2) { 14 | // If this is of the format "7-" with no end range, we set our end range to the totalLines param. totalLines defines how many lines 15 | // are in the gist 16 | if (line[line.length - 1] === '-' && totalLines != null) { 17 | end = totalLines; 18 | } 19 | for (let i = start; i <= end; i++) { 20 | lineNumbers.push(i); 21 | } 22 | } 23 | // If it's just a single line number, push it 24 | else if (range.length === 1) { 25 | lineNumbers.push(parseInt(range[0], 10)); 26 | } 27 | }); 28 | return lineNumbers; 29 | } 30 | 31 | export default getLineNumbers; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gist-embed", 3 | "version": "1.0.4", 4 | "description": "Lightning fast zero dependency library for embedding gists on your webpage", 5 | "main": "gist-embed.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/bvanderhoof/gist-embed.git" 9 | }, 10 | "keywords": [ 11 | "gist", 12 | "embed" 13 | ], 14 | "author": "Blair Vanderhoof", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/bvanderhoof/gist-embed/issues" 18 | }, 19 | "homepage": "https://github.com/bvanderhoof/gist-embed#readme", 20 | "scripts": { 21 | "dev": "webpack-dev-server --env.mode development --hot", 22 | "prod": "webpack --env.mode production", 23 | "debug": "node --inspect-brk ./node_modules/webpack/bin/webpack.js", 24 | "prod:debug": "npm run debug -- --env.mode production", 25 | "dev:debug": "npm run debug -- --env.mode development", 26 | "lint": "eslint . --ext .ts,.tsx", 27 | "test": "jest", 28 | "test:coverage": "npm run test -- --coverage", 29 | "validate": "npm-run-all --parallel lint test", 30 | "check-types": "tsc" 31 | }, 32 | "dependencies": { 33 | "@babel/core": "^7.3.4", 34 | "@babel/preset-env": "^7.3.4", 35 | "@babel/preset-typescript": "^7.3.3", 36 | "babel-loader": "^8.0.5", 37 | "react-dom": "^16.8.4", 38 | "webpack": "^4.29.6", 39 | "webpack-cli": "^3.2.3", 40 | "webpack-merge": "^4.2.1" 41 | }, 42 | "devDependencies": { 43 | "@babel/polyfill": "^7.2.5", 44 | "@types/jest": "^24.0.11", 45 | "@typescript-eslint/eslint-plugin": "^1.4.2", 46 | "@typescript-eslint/parser": "^1.4.2", 47 | "babel-plugin-rewire": "^1.2.0", 48 | "eslint": "^5.15.1", 49 | "eslint-config-prettier": "^4.1.0", 50 | "eslint-loader": "^2.1.2", 51 | "fork-ts-checker-webpack-plugin": "^1.0.0", 52 | "husky": "^1.3.1", 53 | "jest": "^24.3.1", 54 | "lint-staged": "^8.1.5", 55 | "prettier": "^1.16.4", 56 | "typescript": "^3.3.3333", 57 | "webpack-dev-server": "^3.2.1" 58 | }, 59 | "husky": { 60 | "hooks": { 61 | "pre-commit": "npm test && npm run prod && lint-staged && git add ." 62 | } 63 | }, 64 | "lint-staged": { 65 | "*.{js,md,css,html,jsx,ts,tsx}": [ 66 | "prettier --write" 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/highlighLine.test.js: -------------------------------------------------------------------------------- 1 | import highlightLine from '../src/modifiers/highlightLine.ts'; 2 | 3 | function generateMockElement(count) { 4 | const table = document.createElement('table'); 5 | const tbody = document.createElement('tbody'); 6 | 7 | for (let i = 1; i <= count; i++) { 8 | const tr = document.createElement('tr'); 9 | const td = document.createElement('td'); 10 | td.classList.add('js-file-line'); 11 | td.setAttribute('data-line-number', `${i}`); 12 | tr.appendChild(td); 13 | tbody.appendChild(tr); 14 | } 15 | table.appendChild(tbody); 16 | 17 | const element = document.createElement('code'); 18 | element.appendChild(table); 19 | document.body.appendChild(element); 20 | 21 | return element; 22 | } 23 | 24 | function testLinesAreHighlighted(element, indexArr) { 25 | element.querySelectorAll('table tbody tr').forEach((tr, index) => { 26 | if (indexArr.includes(`${index + 1}`) === true) { 27 | expect(tr.querySelector('td').style.backgroundColor).toEqual( 28 | 'rgb(255, 255, 204)', 29 | ); 30 | } else { 31 | expect(tr.querySelector('td').style.backgroundColor).toBeFalsy(); 32 | } 33 | }); 34 | } 35 | 36 | test('line single', () => { 37 | const element = generateMockElement(10); 38 | highlightLine(element, '2'); 39 | testLinesAreHighlighted(element, ['2']); 40 | }); 41 | 42 | test('line commas', () => { 43 | const element = generateMockElement(10); 44 | highlightLine(element, '2,3,4'); 45 | testLinesAreHighlighted(element, ['2', '3', '4']); 46 | }); 47 | 48 | test('line range', () => { 49 | const element = generateMockElement(10); 50 | highlightLine(element, '1-10'); 51 | testLinesAreHighlighted(element, [ 52 | '1', 53 | '2', 54 | '3', 55 | '4', 56 | '5', 57 | '6', 58 | '7', 59 | '8', 60 | '9', 61 | '10', 62 | ]); 63 | }); 64 | 65 | test('line range and commas', () => { 66 | const element = generateMockElement(10); 67 | highlightLine(element, '2,3,4,5-7'); 68 | testLinesAreHighlighted(element, ['2', '3', '4', '5', '6', '7']); 69 | }); 70 | 71 | test('line range all', () => { 72 | const element = generateMockElement(10); 73 | highlightLine(element, '2-'); 74 | testLinesAreHighlighted(element, [ 75 | '2', 76 | '3', 77 | '4', 78 | '5', 79 | '6', 80 | '7', 81 | '8', 82 | '9', 83 | '10', 84 | ]); 85 | }); 86 | -------------------------------------------------------------------------------- /tests/line.test.js: -------------------------------------------------------------------------------- 1 | import line from '../src/modifiers/line.ts'; 2 | 3 | function generateMockElement(count) { 4 | const table = document.createElement('table'); 5 | const tbody = document.createElement('tbody'); 6 | 7 | for (let i = 1; i <= count; i++) { 8 | const tr = document.createElement('tr'); 9 | const td = document.createElement('td'); 10 | td.classList.add('js-file-line'); 11 | td.setAttribute('data-line-number', `${i}`); 12 | tr.appendChild(td); 13 | tbody.appendChild(tr); 14 | } 15 | table.appendChild(tbody); 16 | 17 | const element = document.createElement('code'); 18 | element.appendChild(table); 19 | document.body.appendChild(element); 20 | 21 | return element; 22 | } 23 | 24 | function testLinesReturnedMatchRange(element, indexArr) { 25 | element.querySelectorAll('table tbody tr').forEach((tr, index) => { 26 | expect(tr.querySelector('td').getAttribute('data-line-number')).toEqual( 27 | indexArr[index], 28 | ); 29 | }); 30 | } 31 | 32 | test('line single', () => { 33 | const element = generateMockElement(10); 34 | line(element, '2'); 35 | expect(element.querySelectorAll('table tbody tr').length).toEqual(1); 36 | testLinesReturnedMatchRange(element, ['2']); 37 | }); 38 | 39 | test('line commas', () => { 40 | const element = generateMockElement(10); 41 | line(element, '2,3,4'); 42 | expect(element.querySelectorAll('table tbody tr').length).toEqual(3); 43 | testLinesReturnedMatchRange(element, ['2', '3', '4']); 44 | }); 45 | 46 | test('line range', () => { 47 | const element = generateMockElement(10); 48 | line(element, '1-10'); 49 | expect(element.querySelectorAll('table tbody tr').length).toEqual(10); 50 | testLinesReturnedMatchRange(element, [ 51 | '1', 52 | '2', 53 | '3', 54 | '4', 55 | '5', 56 | '6', 57 | '7', 58 | '8', 59 | '9', 60 | '10', 61 | ]); 62 | }); 63 | 64 | test('line range and commas', () => { 65 | const element = generateMockElement(10); 66 | line(element, '2,3,4,5-7'); 67 | expect(element.querySelectorAll('table tbody tr').length).toEqual(6); 68 | testLinesReturnedMatchRange(element, ['2', '3', '4', '5', '6', '7']); 69 | }); 70 | 71 | test('line range all', () => { 72 | const element = generateMockElement(10); 73 | line(element, '2-'); 74 | expect(element.querySelectorAll('table tbody tr').length).toEqual(9); 75 | testLinesReturnedMatchRange(element, [ 76 | '2', 77 | '3', 78 | '4', 79 | '5', 80 | '6', 81 | '7', 82 | '8', 83 | '9', 84 | '10', 85 | ]); 86 | }); 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gist-embed 2 | 3 | Lightning fast zero dependency library for embedding gists on your webpage. 4 | 5 | # Usage 6 | 7 | ## Add script tag 8 | 9 | ```html 10 | 14 | ``` 15 | 16 | ## Add code element to your webpage with data-gist-id attribute 17 | 18 | ```html 19 | 20 | ``` 21 | 22 | ## Global settings and init 23 | 24 | - You can add an object with settings to `window.GistEmbedSettings` before you include the gist-embed script tag 25 | - Settings supported: 26 | - `baseURL`: Specify a base url used to fetch the gist. Defaults to `https://gist.github.com`. 27 | - You can run the init to parse the elements at any time. This is useful when your code element is added after the script tag include has already parsed the elements on page load. 28 | - `window.GistEmbed.init()`: This will parse all the elements again that have a `data-gist-id` attribute. 29 | 30 | ## Modifiers 31 | 32 | You can add attributes to your HTML Element that modify the gist embed. 33 | 34 | - `data-gist-hide-line-numbers` 35 | - **type**: string `('true')` 36 | - Removes all of the line numbers in the left hand gutter of the gist 37 | - `data-gist-hide-footer` 38 | - **type**: string `('true')` 39 | - Removes the gist footer 40 | - `data-gist-caption` 41 | - **type**: string 42 | - Places a header above the gist with your chosen caption string 43 | - `data-gist-file` 44 | - **type**: string 45 | - If the gist has multiple files, specify the filename you want to show 46 | - `data-gist-line` 47 | - **type**: string 48 | - Line numbers you want to show. The rest are removed. 49 | - Examples: 50 | - `1,2,3` // Only shows lines 1, 2 and 3 51 | - `1-4` // Only shows lines 1, 2, 3, and 4 52 | - `1-4,8` // Only shows lines 1, 2, 3, 4, and 8 53 | - `1-` // Shows lines 1 to the end 54 | - `data-gist-highlight-line` 55 | - **type**: string 56 | - Line numbers you want to highlight. Uses the same syntax for line ranges as `data-gist-line` 57 | 58 | # Contributing 59 | 60 | ## Setup 61 | 62 | - I recommend using VSCode to develop 63 | - Install prettier VSCode extension 64 | - `npm install` 65 | 66 | ## Development 67 | 68 | - To start the dev server: `npm run dev` 69 | - This starts webpack with a local web server and hot reloading 70 | - navigate to http://localhost:8080/ 71 | - webpack serves the compiled TypeScript `index.ts` to `/dist/gist-embed.min.js` when in development mode 72 | 73 | ## Tests 74 | 75 | - Please add unit tests for your new code. 76 | - `npm test` 77 | - Use `Rewire` methods to get access to private functions in `index.ts`. See `tests/index.test.js` for examples. 78 | - Add an example to `index.html` 79 | 80 | ## Committing 81 | 82 | - Update the README with the new jsdelivr script based on new version bump 83 | - Husky, a git hook tool, will automatically lint and run prettier when you `git commit` as well as run jest tests and create the prod minified bundle. 84 | 85 | # FAQ 86 | 87 | ## What happened to blairvanderhoof/gist-embed based on jQuery? 88 | 89 | - My old github account couldn't be recovered, so starting with a new repo here. 90 | - I always wanted to rewrite gist-embed to not use jQuery and simplify the code. 91 | - Going forward, this will be where all gist-embed code will reside. 92 | 93 | ## Can I still use blairvanderhoof/gist-embed? 94 | 95 | - That account will probably go away soon. If you really need to rely on it, I suggest forking it before it's removed. 96 | 97 | ## But I like using jQuery! 98 | 99 | - This will be a plain vanilla JS library from now on. 100 | - The gists should load much faster now that we are removing jQuery as a dependency. 101 | 102 | ## How did you lose your old account? 103 | 104 | - Lesson learned - always keep up to date 2FA recovery codes if you have 2FA enabled on your account and lose your authentication app :( 105 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | "jsx": "preserve" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | "sourceMap": true /* Generates corresponding '.map' file. */, 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./dist/" /* Redirect output structure to the directory. */, 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | "noEmit": true /* Do not emit outputs. */, 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | 23 | /* Strict Type-Checking Options */ 24 | "strict": true /* Enable all strict type-checking options. */, 25 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */, 26 | "strictNullChecks": true /* Enable strict null checks. */, 27 | "strictFunctionTypes": false /* Enable strict checking of function types. */, 28 | "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, 29 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 30 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 31 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 32 | 33 | /* Additional Checks */ 34 | "noUnusedLocals": true /* Report errors on unused locals. */, 35 | "noUnusedParameters": true /* Report errors on unused parameters. */, 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 38 | 39 | /* Module Resolution Options */ 40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 47 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | 50 | /* Source Map Options */ 51 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | 60 | "skipLibCheck": true 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | gist-embed 5 | 9 | 13 | 14 | 15 | 86 | 87 | 88 |
89 |

90 | gist-embed 91 |

92 |

93 | Lightning fast zero dependency library for embedding gists on your 94 | website 95 |

96 |

97 | 98 | 99 | 100 | Documentation and source on Github 101 | 102 | 103 |
104 | written by 105 | Blair Vanderhoof 106 |
107 |

108 |
109 | 110 |

Loading a gist

111 | 112 | 113 |

Loading a gist with all line numbers removed

114 | 118 | 119 |

Loading a gist with footer removed

120 | 124 | 125 |

Loading a gist with both footer and line numbers removed

126 | 131 | 132 |

Loading a gist with caption

133 | 137 | 138 |

Loading a gist with caption with footer removed

139 | 144 | 145 |

Loading a gist with multiple files

146 | 147 | 148 |

Loading a single file from a gist (example7-file2.html)

149 | 153 | 154 |

Loading a single line number from a gist (line 2)

155 | 159 | 160 |

Loading a range of line numbers from a gist (line 2 through 4)

161 | 165 | 166 |

167 | Loading a single line and a range of line numbers from a gist (line 1 and 168 | line 3 through 4) 169 |

170 | 174 | 175 |

Highlighting a list of line numbers from a gist (line 1, 3, 5)

176 | 180 | 181 |

182 | Loading a range of lines automatically using a trailing "-" (line 2 183 | through 5) 184 |

185 | 189 | 190 |

191 | Highlight a range of lines automatically using a trailing "-" (line 2 192 | through 5) 193 |

194 | 198 | 199 |

Loading a code element without a gist id data attribute

200 | 201 | This is the content of a code element. It has no gist id data attribute, 202 | so it's not parsed. 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import caption from './modifiers/caption'; 2 | import hideFooter from './modifiers/hideFooter'; 3 | import hideLineNumbers from './modifiers/hideLineNumbers'; 4 | import highlightLine from './modifiers/highlightLine'; 5 | import line from './modifiers/line'; 6 | 7 | // Global methods 8 | const GIST_EMBED_GLOBAL_FUNC_NAME = 'GistEmbed'; 9 | const GIST_EMBED_GLOBAL_SETTINGS_NAME = 'GistEmbedSettings'; 10 | const GIST_EMBED_GLOBAL_SETTINGS_BASE_URL_NAME = 'baseURL'; 11 | window[GIST_EMBED_GLOBAL_SETTINGS_NAME] = 12 | window[GIST_EMBED_GLOBAL_SETTINGS_NAME] || {}; 13 | window[GIST_EMBED_GLOBAL_FUNC_NAME] = {}; 14 | 15 | // Keep track of stylesheets added. Only append a new stylesheet if it doesn't exist 16 | const StylesheetURLs: Set = new Set(); 17 | // Prefix for fetching via JSONP 18 | const JSONP_CALLBACK_PREFIX: string = '_gistEmbedJSONP_'; 19 | // Global counter for each JSONP called so we can append to prefix to create a unique JSONP callback 20 | let _jsonpCallbackIDCounter: number = 0; 21 | // URL prefix to get the JSONP result 22 | // You can sepcify a base url if you'd like 23 | const GIST_URL_PREFIX: string = 24 | window[GIST_EMBED_GLOBAL_SETTINGS_NAME][ 25 | GIST_EMBED_GLOBAL_SETTINGS_BASE_URL_NAME 26 | ] || 'https://gist.github.com/'; 27 | // The attribute we check on the DOM elements to grab the gist id 28 | const GIST_ID_ATTRIBUTE_NAME: string = 'data-gist-id'; 29 | // Attribute used to specify file to fetch during the request in case the gist is multi-file 30 | const GIST_FILE_ATTRIBUTE_NAME: string = 'data-gist-file'; 31 | 32 | enum MODIFIER_ATTRIBUTES { 33 | hideLineNumbersAttribute = 'data-gist-hide-line-numbers', 34 | hideFooterAttribute = 'data-gist-hide-footer', 35 | captionAttribute = 'data-gist-caption', 36 | lineAttribute = 'data-gist-line', 37 | highlightLineAttribute = 'data-gist-highlight-line', 38 | } 39 | const MODIFIER_ATTRIBUTE_NAMES: MODIFIER_ATTRIBUTES[] = [ 40 | MODIFIER_ATTRIBUTES.hideLineNumbersAttribute, 41 | MODIFIER_ATTRIBUTES.hideFooterAttribute, 42 | MODIFIER_ATTRIBUTES.captionAttribute, 43 | MODIFIER_ATTRIBUTES.lineAttribute, 44 | MODIFIER_ATTRIBUTES.highlightLineAttribute, 45 | ]; 46 | 47 | type GistJSONResponse = 48 | | { 49 | div?: string | undefined; 50 | stylesheet?: string | undefined; 51 | } 52 | | null 53 | | undefined; 54 | 55 | // document ready, call init 56 | if (document.readyState === 'complete') { 57 | init(); 58 | } else { 59 | document.addEventListener('DOMContentLoaded', init); 60 | } 61 | 62 | // init function that runs on page load. Grabs all nodes and fetches JSONP for each and 63 | // swapping the content of the node with the response 64 | function init() { 65 | Array.from(getAllGistEmbedDOMNodes()).forEach(fetchJSONPForGistEmbedDOMNode); 66 | } 67 | 68 | // Expose init so we can execute it after dom ready if desired 69 | window[GIST_EMBED_GLOBAL_FUNC_NAME].init = init; 70 | 71 | // returns all dom nodes with attribute GIST_ID_ATTRIBUTE_NAME 72 | function getAllGistEmbedDOMNodes(): NodeList { 73 | return document.querySelectorAll(`[${GIST_ID_ATTRIBUTE_NAME}]`); 74 | } 75 | 76 | // creates a unique callback for JSONP 77 | function generateJSONPCallbackPrefix(): string { 78 | ++_jsonpCallbackIDCounter; 79 | return `${JSONP_CALLBACK_PREFIX}${_jsonpCallbackIDCounter}`; 80 | } 81 | 82 | // Add a stylesheet to the DOM given a http url 83 | function appendStylesheet(stylesheetURL: string) { 84 | if (!StylesheetURLs.has(stylesheetURL)) { 85 | StylesheetURLs.add(stylesheetURL); 86 | const linkEl = document.createElement('link'); 87 | linkEl.setAttribute('href', stylesheetURL); 88 | linkEl.setAttribute('type', 'text/css'); 89 | linkEl.setAttribute('rel', 'stylesheet'); 90 | document.body.appendChild(linkEl); 91 | } 92 | } 93 | 94 | // Simple getJSONP method that takes a gist id and callback 95 | function getJSONP( 96 | gistID: string, 97 | fileName: string | undefined | null, 98 | callback: (response: GistJSONResponse) => void, 99 | ) { 100 | const callbackName = generateJSONPCallbackPrefix(); 101 | window[callbackName] = callback; 102 | 103 | const scriptEl = document.createElement('script'); 104 | const fileQueryParam = 105 | fileName != null ? `&file=${encodeURIComponent(fileName)}` : ''; 106 | scriptEl.setAttribute( 107 | 'src', 108 | `${GIST_URL_PREFIX}${gistID}.json?callback=${callbackName}${fileQueryParam}`, 109 | ); 110 | document.body.appendChild(scriptEl); 111 | } 112 | 113 | // Fetch the JSONP for a given DOM Node 114 | function fetchJSONPForGistEmbedDOMNode(gistDOMNode: HTMLElement) { 115 | const gistID = gistDOMNode.getAttribute(GIST_ID_ATTRIBUTE_NAME); 116 | const fileName = gistDOMNode.getAttribute(GIST_FILE_ATTRIBUTE_NAME); 117 | if (gistID != null && gistID !== '') { 118 | getJSONP(gistID, fileName, function(response: GistJSONResponse) { 119 | handleGetJSONPResponse(gistDOMNode, response); 120 | }); 121 | } 122 | } 123 | 124 | function handleGetJSONPResponse( 125 | gistDOMNode: HTMLElement, 126 | response: GistJSONResponse, 127 | ) { 128 | if (response == null || response.div == null || response.stylesheet == null) { 129 | gistDOMNode.innerHTML = 'Error fetching gist'; 130 | return; 131 | } 132 | 133 | updateDOMNodeWithGistContent(gistDOMNode, response.stylesheet, response.div); 134 | } 135 | 136 | // From the JSONP response, add the stylesheet to the DOM and replace the DOM Node contents 137 | function updateDOMNodeWithGistContent( 138 | gistDOMNode: HTMLElement, 139 | responseStylesheet: string, 140 | responseDIV: string, 141 | ) { 142 | appendStylesheet(responseStylesheet); 143 | // update 144 | gistDOMNode.innerHTML = responseDIV; 145 | // Avoid id collision. id is the gistID and we could be embedding multiple on the page 146 | if (gistDOMNode.children.length) { 147 | gistDOMNode.children[0].removeAttribute('id'); 148 | } 149 | 150 | modify(gistDOMNode); 151 | } 152 | 153 | function modify(gistDOMNode: HTMLElement) { 154 | MODIFIER_ATTRIBUTE_NAMES.forEach(attribute => { 155 | const attributeValue = gistDOMNode.getAttribute(attribute); 156 | if (attributeValue != null && attributeValue !== '') { 157 | switch (attribute) { 158 | case 'data-gist-hide-line-numbers': 159 | if (attributeValue === 'true') { 160 | hideLineNumbers(gistDOMNode); 161 | } 162 | break; 163 | case 'data-gist-hide-footer': 164 | if (attributeValue === 'true') { 165 | hideFooter(gistDOMNode); 166 | } 167 | break; 168 | case 'data-gist-caption': 169 | caption(gistDOMNode, attributeValue); 170 | break; 171 | case 'data-gist-line': 172 | line(gistDOMNode, attributeValue); 173 | break; 174 | case 'data-gist-highlight-line': 175 | highlightLine(gistDOMNode, attributeValue); 176 | break; 177 | } 178 | } 179 | }); 180 | } 181 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | // Note: 2 | // Since the functions in index.ts are not exported, we use __get__ to grab them 3 | // This comes from the babel rewire plugin setup in babelrc 4 | // To modify a function use __Rewire__ 5 | 6 | // Must be require so we can re-require and override index on each pass 7 | let index = require('../src/index.ts'); 8 | let hideLineNumbers = require('../src/modifiers/hideLineNumbers.ts'); 9 | 10 | const JSONP_CALLBACK_PREFIX = '_gistEmbedJSONP'; 11 | const MOCK_RESPONSE = { 12 | div: 'content', 13 | stylesheet: 'https://www.github.com/main.css', 14 | }; 15 | const MOCK_ERROR_RESPONSE = null; 16 | 17 | function generateMockElement() { 18 | const gistID = '1'; 19 | const element = document.createElement('code'); 20 | element.setAttribute('data-gist-id', gistID); 21 | return element; 22 | } 23 | 24 | beforeEach(() => { 25 | document.body.innerHTML = ''; 26 | jest.clearAllMocks().resetModules(); 27 | __rewire_reset_all__(); 28 | jest.mock('../src/modifiers/hideLineNumbers.ts'); 29 | // Re-require before each test so we reset private scope variables 30 | index = require('../src/index.ts'); 31 | hideLineNumbers = require('../src/modifiers/hideLineNumbers.ts'); 32 | }); 33 | 34 | function addPlainGistEmbedDOMToBody() { 35 | document.body.innerHTML = ''; 36 | } 37 | 38 | function getFN(fnName) { 39 | return index.__get__(fnName); 40 | } 41 | 42 | function rewireFn(fnName, value) { 43 | return index.__Rewire__(fnName, value); 44 | } 45 | 46 | test('window exists', () => { 47 | expect(window.GistEmbed.init).toEqual(getFN('init')); 48 | expect(window.GistEmbedSettings).toBeTruthy(); 49 | }); 50 | 51 | test('expect getAllGistEmbedDOMNodes to be called from index', () => { 52 | rewireFn('getAllGistEmbedDOMNodes', jest.fn(() => [{}, {}])); 53 | rewireFn('fetchJSONPForGistEmbedDOMNode', jest.fn(() => {})); 54 | getFN('init')(); 55 | expect(getFN('getAllGistEmbedDOMNodes')).toHaveBeenCalledTimes(1); 56 | }); 57 | 58 | test('expect fetchJSONPForGistEmbedDOMNode to be called from index', () => { 59 | rewireFn('fetchJSONPForGistEmbedDOMNode', jest.fn(() => {})); 60 | addPlainGistEmbedDOMToBody(); 61 | getFN('init')(); 62 | expect(getFN('fetchJSONPForGistEmbedDOMNode')).toHaveBeenCalledTimes(1); 63 | }); 64 | 65 | test('expect getAllGistEmbedDOMNodes to return a node ', () => { 66 | addPlainGistEmbedDOMToBody(); 67 | expect(getFN('getAllGistEmbedDOMNodes')().length).toEqual(1); 68 | }); 69 | 70 | test('expect getAllGistEmbedDOMNodes to not return nodes', () => { 71 | expect(getFN('getAllGistEmbedDOMNodes')().length).toEqual(0); 72 | }); 73 | 74 | test('generateJSONPCallbackPrefix returns incremented values', () => { 75 | const generateJSONPCallbackPrefix = getFN('generateJSONPCallbackPrefix'); 76 | expect(generateJSONPCallbackPrefix()).toEqual(`${JSONP_CALLBACK_PREFIX}_1`); 77 | expect(generateJSONPCallbackPrefix()).toEqual(`${JSONP_CALLBACK_PREFIX}_2`); 78 | }); 79 | 80 | test('appendStylesheet appends if not exists', () => { 81 | const appendStylesheet = getFN('appendStylesheet'); 82 | const mainCSSURL = 'https://www.github.com/main.css'; 83 | const main2CSSURL = 'https://www.github.com/main2.css'; 84 | 85 | // First time append adds it 86 | appendStylesheet(mainCSSURL); 87 | let linkTags = document.querySelectorAll('link'); 88 | expect(linkTags.length).toEqual(1); 89 | expect(linkTags[0].getAttribute('href')).toEqual(mainCSSURL); 90 | 91 | // Second time append same url doesn't add it 92 | appendStylesheet(mainCSSURL); 93 | linkTags = document.querySelectorAll('link'); 94 | expect(linkTags.length).toEqual(1); 95 | 96 | // Third time append different url adds it 97 | appendStylesheet(main2CSSURL); 98 | linkTags = document.querySelectorAll('link'); 99 | expect(linkTags[1].getAttribute('href')).toEqual(main2CSSURL); 100 | expect(linkTags.length).toEqual(2); 101 | }); 102 | 103 | test('getJSONP', () => { 104 | const getJSONP = getFN('getJSONP'); 105 | const gistID = '1'; 106 | const callback = jest.fn(); 107 | const fileName = 'foo.html'; 108 | 109 | getJSONP(gistID, fileName, callback); 110 | let scriptTags = document.querySelectorAll('script'); 111 | expect(scriptTags.length).toEqual(1); 112 | expect(scriptTags[0].src).toEqual( 113 | `https://gist.github.com/1.json?callback=${JSONP_CALLBACK_PREFIX}_1&file=${fileName}`, 114 | ); 115 | expect(window[`${JSONP_CALLBACK_PREFIX}_1`]).toBeTruthy(); 116 | }); 117 | 118 | test('fetchJSONPForGistEmbedDOMNode', () => { 119 | const fetchJSONPForGistEmbedDOMNode = getFN('fetchJSONPForGistEmbedDOMNode'); 120 | rewireFn('getJSONP', jest.fn((_, __, cb) => cb(MOCK_RESPONSE))); 121 | const getJSONP = getFN('getJSONP'); 122 | rewireFn('handleGetJSONPResponse', jest.fn()); 123 | const handleGetJSONPResponse = getFN('handleGetJSONPResponse'); 124 | const element = generateMockElement(); 125 | 126 | fetchJSONPForGistEmbedDOMNode(element); 127 | expect(getJSONP).toBeCalledTimes(1); 128 | expect(handleGetJSONPResponse).toBeCalledWith(element, MOCK_RESPONSE); 129 | }); 130 | 131 | test('handleGetJSONPResponse success', () => { 132 | const handleGetJSONPResponse = getFN('handleGetJSONPResponse'); 133 | rewireFn('updateDOMNodeWithGistContent', jest.fn()); 134 | const updateDOMNodeWithGistContent = getFN('updateDOMNodeWithGistContent'); 135 | const element = generateMockElement(); 136 | 137 | handleGetJSONPResponse(element, MOCK_RESPONSE); 138 | expect(updateDOMNodeWithGistContent).toBeCalledWith( 139 | element, 140 | MOCK_RESPONSE.stylesheet, 141 | MOCK_RESPONSE.div, 142 | ); 143 | }); 144 | 145 | test('handleGetJSONPResponse error', () => { 146 | const handleGetJSONPResponse = getFN('handleGetJSONPResponse'); 147 | rewireFn('updateDOMNodeWithGistContent', jest.fn()); 148 | const updateDOMNodeWithGistContent = getFN('updateDOMNodeWithGistContent'); 149 | const element = generateMockElement(); 150 | document.body.appendChild(element); 151 | 152 | // Test error handling 153 | handleGetJSONPResponse(element, MOCK_ERROR_RESPONSE); 154 | expect(updateDOMNodeWithGistContent).not.toBeCalled(); 155 | expect(document.querySelector('code').innerHTML).toEqual( 156 | 'Error fetching gist', 157 | ); 158 | }); 159 | 160 | test('updateDOMNodeWithGistContent', () => { 161 | const updateDOMNodeWithGistContent = getFN('updateDOMNodeWithGistContent'); 162 | rewireFn('modify', jest.fn()); 163 | const modify = getFN('modify'); 164 | const element = generateMockElement(); 165 | document.body.appendChild(element); 166 | 167 | updateDOMNodeWithGistContent( 168 | element, 169 | MOCK_RESPONSE.stylesheet, 170 | MOCK_RESPONSE.div, 171 | ); 172 | expect(document.querySelector('code').innerHTML).toEqual(MOCK_RESPONSE.div); 173 | expect(document.querySelector('link').getAttribute('href')).toEqual( 174 | MOCK_RESPONSE.stylesheet, 175 | ); 176 | expect(modify).toBeCalledWith(element); 177 | }); 178 | 179 | test('modify', () => { 180 | const modify = getFN('modify'); 181 | const element = generateMockElement(); 182 | element.setAttribute('data-gist-hide-line-numbers', 'true'); 183 | document.body.appendChild(element); 184 | 185 | modify(element); 186 | 187 | expect(hideLineNumbers.default).toBeCalledWith(element); 188 | }); 189 | -------------------------------------------------------------------------------- /dist/gist-embed.min.js: -------------------------------------------------------------------------------- 1 | !function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=74)}([function(t,e,n){var r=n(28)("wks"),o=n(21),i=n(1).Symbol,u="function"==typeof i;(t.exports=function(t){return r[t]||(r[t]=u&&i[t]||(u?i:o)("Symbol."+t))}).store=r},function(t,e){var n=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(t,e){t.exports=function(t){return"object"==typeof t?null!==t:"function"==typeof t}},function(t,e,n){var r=n(4),o=n(45),i=n(33),u=Object.defineProperty;e.f=n(5)?Object.defineProperty:function(t,e,n){if(r(t),e=i(e,!0),r(n),o)try{return u(t,e,n)}catch(t){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(t[e]=n.value),t}},function(t,e,n){var r=n(2);t.exports=function(t){if(!r(t))throw TypeError(t+" is not an object!");return t}},function(t,e,n){t.exports=!n(6)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(t,e){t.exports=function(t){try{return!!t()}catch(t){return!0}}},function(t,e,n){var r=n(1),o=n(18),i=n(8),u=n(14),c=n(23),_=function(t,e,n){var f,a,l,s,p=t&_.F,y=t&_.G,h=t&_.S,v=t&_.P,d=t&_.B,b=y?r:h?r[e]||(r[e]={}):(r[e]||{}).prototype,E=y?o:o[e]||(o[e]={}),g=E.prototype||(E.prototype={});for(f in y&&(n=e),n)l=((a=!p&&b&&void 0!==b[f])?b:n)[f],s=d&&a?c(l,r):v&&"function"==typeof l?c(Function.call,l):l,b&&u(b,f,l,t&_.U),E[f]!=l&&i(E,f,s),v&&g[f]!=l&&(g[f]=l)};r.core=o,_.F=1,_.G=2,_.S=4,_.P=8,_.B=16,_.W=32,_.U=64,_.R=128,t.exports=_},function(t,e,n){var r=n(3),o=n(22);t.exports=n(5)?function(t,e,n){return r.f(t,e,o(1,n))}:function(t,e,n){return t[e]=n,t}},function(t,e,n){"use strict";var r=n(59),o=n(60),i=n(26),u=n(19);t.exports=n(41)(Array,"Array",function(t,e){this._t=u(t),this._i=0,this._k=e},function(){var t=this._t,e=this._k,n=this._i++;return!t||n>=t.length?(this._t=void 0,o(1)):o(0,"keys"==e?n:"values"==e?t[n]:[n,t[n]])},"values"),i.Arguments=i.Array,r("keys"),r("values"),r("entries")},function(t,e){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(t){"object"==typeof window&&(n=window)}t.exports=n},function(t,e,n){n(43)("asyncIterator")},function(t,e,n){"use strict";var r=n(1),o=n(13),i=n(5),u=n(7),c=n(14),_=n(34).KEY,f=n(6),a=n(28),l=n(29),s=n(21),p=n(0),y=n(44),h=n(43),v=n(76),d=n(79),b=n(4),E=n(2),g=n(19),O=n(33),R=n(22),m=n(39),L=n(82),S=n(52),I=n(3),w=n(24),x=S.f,j=I.f,A=L.f,T=r.Symbol,G=r.JSON,N=G&&G.stringify,D=p("_hidden"),$=p("toPrimitive"),P={}.propertyIsEnumerable,k=a("symbol-registry"),M=a("symbols"),B=a("op-symbols"),W=Object.prototype,F="function"==typeof T,C=r.QObject,U=!C||!C.prototype||!C.prototype.findChild,Y=i&&f(function(){return 7!=m(j({},"a",{get:function(){return j(this,"a",{value:7}).a}})).a})?function(t,e,n){var r=x(W,e);r&&delete W[e],j(t,e,n),r&&t!==W&&j(W,e,r)}:j,X=function(t){var e=M[t]=m(T.prototype);return e._k=t,e},J=F&&"symbol"==typeof T.iterator?function(t){return"symbol"==typeof t}:function(t){return t instanceof T},q=function(t,e,n){return t===W&&q(B,e,n),b(t),e=O(e,!0),b(n),o(M,e)?(n.enumerable?(o(t,D)&&t[D][e]&&(t[D][e]=!1),n=m(n,{enumerable:R(0,!1)})):(o(t,D)||j(t,D,R(1,{})),t[D][e]=!0),Y(t,e,n)):j(t,e,n)},H=function(t,e){b(t);for(var n,r=v(e=g(e)),o=0,i=r.length;i>o;)q(t,n=r[o++],e[n]);return t},K=function(t){var e=P.call(this,t=O(t,!0));return!(this===W&&o(M,t)&&!o(B,t))&&(!(e||!o(this,t)||!o(M,t)||o(this,D)&&this[D][t])||e)},V=function(t,e){if(t=g(t),e=O(e,!0),t!==W||!o(M,e)||o(B,e)){var n=x(t,e);return!n||!o(M,e)||o(t,D)&&t[D][e]||(n.enumerable=!0),n}},z=function(t){for(var e,n=A(g(t)),r=[],i=0;n.length>i;)o(M,e=n[i++])||e==D||e==_||r.push(e);return r},Q=function(t){for(var e,n=t===W,r=A(n?B:g(t)),i=[],u=0;r.length>u;)!o(M,e=r[u++])||n&&!o(W,e)||i.push(M[e]);return i};F||(c((T=function(){if(this instanceof T)throw TypeError("Symbol is not a constructor!");var t=s(arguments.length>0?arguments[0]:void 0),e=function(n){this===W&&e.call(B,n),o(this,D)&&o(this[D],t)&&(this[D][t]=!1),Y(this,t,R(1,n))};return i&&U&&Y(W,t,{configurable:!0,set:e}),X(t)}).prototype,"toString",function(){return this._k}),S.f=V,I.f=q,n(51).f=L.f=z,n(38).f=K,n(50).f=Q,i&&!n(27)&&c(W,"propertyIsEnumerable",K,!0),y.f=function(t){return X(p(t))}),u(u.G+u.W+u.F*!F,{Symbol:T});for(var Z="hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","),tt=0;Z.length>tt;)p(Z[tt++]);for(var et=w(p.store),nt=0;et.length>nt;)h(et[nt++]);u(u.S+u.F*!F,"Symbol",{for:function(t){return o(k,t+="")?k[t]:k[t]=T(t)},keyFor:function(t){if(!J(t))throw TypeError(t+" is not a symbol!");for(var e in k)if(k[e]===t)return e},useSetter:function(){U=!0},useSimple:function(){U=!1}}),u(u.S+u.F*!F,"Object",{create:function(t,e){return void 0===e?m(t):H(m(t),e)},defineProperty:q,defineProperties:H,getOwnPropertyDescriptor:V,getOwnPropertyNames:z,getOwnPropertySymbols:Q}),G&&u(u.S+u.F*(!F||f(function(){var t=T();return"[null]"!=N([t])||"{}"!=N({a:t})||"{}"!=N(Object(t))})),"JSON",{stringify:function(t){for(var e,n,r=[t],o=1;arguments.length>o;)r.push(arguments[o++]);if(n=e=r[1],(E(e)||void 0!==t)&&!J(t))return d(e)||(e=function(t,e){if("function"==typeof n&&(e=n.call(this,t,e)),!J(e))return e}),r[1]=e,N.apply(G,r)}}),T.prototype[$]||n(8)(T.prototype,$,T.prototype.valueOf),l(T,"Symbol"),l(Math,"Math",!0),l(r.JSON,"JSON",!0)},function(t,e){var n={}.hasOwnProperty;t.exports=function(t,e){return n.call(t,e)}},function(t,e,n){var r=n(1),o=n(8),i=n(13),u=n(21)("src"),c=n(75),_=(""+c).split("toString");n(18).inspectSource=function(t){return c.call(t)},(t.exports=function(t,e,n,c){var f="function"==typeof n;f&&(i(n,"name")||o(n,"name",e)),t[e]!==n&&(f&&(i(n,u)||o(n,u,t[e]?""+t[e]:_.join(String(e)))),t===r?t[e]=n:c?t[e]?t[e]=n:o(t,e,n):(delete t[e],o(t,e,n)))})(Function.prototype,"toString",function(){return"function"==typeof this&&this[u]||c.call(this)})},function(t,e,n){var r=n(3).f,o=Function.prototype,i=/^\s*function ([^ (]*)/;"name"in o||n(5)&&r(o,"name",{configurable:!0,get:function(){try{return(""+this).match(i)[1]}catch(t){return""}}})},function(t,e,n){var r=n(40),o=n(24);n(53)("keys",function(){return function(t){return o(r(t))}})},function(t,e,n){for(var r=n(9),o=n(24),i=n(14),u=n(1),c=n(8),_=n(26),f=n(0),a=f("iterator"),l=f("toStringTag"),s=_.Array,p={CSSRuleList:!0,CSSStyleDeclaration:!1,CSSValueList:!1,ClientRectList:!1,DOMRectList:!1,DOMStringList:!1,DOMTokenList:!0,DataTransferItemList:!1,FileList:!1,HTMLAllCollection:!1,HTMLCollection:!1,HTMLFormElement:!1,HTMLSelectElement:!1,MediaList:!0,MimeTypeArray:!1,NamedNodeMap:!1,NodeList:!0,PaintRequestList:!1,Plugin:!1,PluginArray:!1,SVGLengthList:!1,SVGNumberList:!1,SVGPathSegList:!1,SVGPointList:!1,SVGStringList:!1,SVGTransformList:!1,SourceBufferList:!1,StyleSheetList:!0,TextTrackCueList:!1,TextTrackList:!1,TouchList:!1},y=o(p),h=0;h0?o(r(t),9007199254740991):0}},function(t,e,n){"use strict";(function(t){n(11),n(12),n(20),n(15),n(9),n(16),n(96),n(17);function r(t){return(r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function o(t,e){var n=[];return t.split(",").forEach(function(t){var r=t.split("-"),o=parseInt(r[0],10),i=parseInt(r[1],10);if(2===r.length){"-"===t[t.length-1]&&null!=e&&(i=e);for(var u=o;u<=i;u++)n.push(u)}else 1===r.length&&n.push(parseInt(r[0],10))}),n}function i(){try{if(t)return t}catch(t){try{if(window)return window}catch(t){return this}}}e.a=p("getLineNumbers");var u,c=null;function _(){if(null===c){var t=i();t.__$$GLOBAL_REWIRE_NEXT_MODULE_ID__||(t.__$$GLOBAL_REWIRE_NEXT_MODULE_ID__=0),c=__$$GLOBAL_REWIRE_NEXT_MODULE_ID__++}return c}function f(){var t=i();return t.__$$GLOBAL_REWIRE_REGISTRY__||(t.__$$GLOBAL_REWIRE_REGISTRY__=Object.create(null)),t.__$$GLOBAL_REWIRE_REGISTRY__}function a(){var t=_(),e=f(),n=e[t];return n||(e[t]=Object.create(null),n=e[t]),n}(u=i()).__rewire_reset_all__||(u.__rewire_reset_all__=function(){u.__$$GLOBAL_REWIRE_REGISTRY__=Object.create(null)});var l="__INTENTIONAL_UNDEFINED__",s={};function p(t){var e=a();if(void 0===e[t])return function(t){switch(t){case"getLineNumbers":return o}return}(t);var n=e[t];return n===l?void 0:n}function y(t,e){var n=a();return"object"===r(t)?(Object.keys(t).forEach(function(e){n[e]=t[e]}),function(){Object.keys(t).forEach(function(e){h(t)})}):(n[t]=void 0===e?l:e,function(){h(t)})}function h(t){var e=a();delete e[t],0==Object.keys(e).length&&delete f()[_]}function v(t){var e=a(),n=Object.keys(t),r={};function o(){n.forEach(function(t){e[t]=r[t]})}return function(i){n.forEach(function(n){r[n]=e[n],e[n]=t[n]});var u=i();return u&&"function"==typeof u.then?u.then(o).catch(o):o(),u}}!function(){function t(t,e){Object.defineProperty(s,t,{value:e,enumerable:!1,configurable:!0})}t("__get__",p),t("__GetDependency__",p),t("__Rewire__",y),t("__set__",y),t("__reset__",h),t("__ResetDependency__",h),t("__with__",v)}();var d=r(o);function b(t,e){Object.defineProperty(o,t,{value:e,enumerable:!1,configurable:!0})}"object"!==d&&"function"!==d||!Object.isExtensible(o)||(b("__get__",p),b("__GetDependency__",p),b("__Rewire__",y),b("__set__",y),b("__reset__",h),b("__ResetDependency__",h),b("__with__",v),b("__RewireAPI__",s))}).call(this,n(10))},function(t,e,n){var r=n(2);t.exports=function(t,e){if(!r(t))return t;var n,o;if(e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;if("function"==typeof(n=t.valueOf)&&!r(o=n.call(t)))return o;if(!e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},function(t,e,n){var r=n(21)("meta"),o=n(2),i=n(13),u=n(3).f,c=0,_=Object.isExtensible||function(){return!0},f=!n(6)(function(){return _(Object.preventExtensions({}))}),a=function(t){u(t,r,{value:{i:"O"+ ++c,w:{}}})},l=t.exports={KEY:r,NEED:!1,fastKey:function(t,e){if(!o(t))return"symbol"==typeof t?t:("string"==typeof t?"S":"P")+t;if(!i(t,r)){if(!_(t))return"F";if(!e)return"E";a(t)}return t[r].i},getWeak:function(t,e){if(!i(t,r)){if(!_(t))return!0;if(!e)return!1;a(t)}return t[r].w},onFreeze:function(t){return f&&l.NEED&&_(t)&&!i(t,r)&&a(t),t}}},function(t,e){var n=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:n)(t)}},function(t,e,n){var r=n(28)("keys"),o=n(21);t.exports=function(t){return r[t]||(r[t]=o(t))}},function(t,e){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(t,e){e.f={}.propertyIsEnumerable},function(t,e,n){var r=n(4),o=n(80),i=n(37),u=n(36)("IE_PROTO"),c=function(){},_=function(){var t,e=n(46)("iframe"),r=i.length;for(e.style.display="none",n(81).appendChild(e),e.src="javascript:",(t=e.contentWindow.document).open(),t.write("