Repository: liriliri/eruda
Branch: master
Commit: 0c55928fec80
Files: 99
Total size: 352.1 KB
Directory structure:
gitextract_e87c_nux/
├── .eustia.js
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ ├── main.yml
│ └── publish.yml
├── .gitignore
├── .gitmodules
├── .prettierignore
├── .prettierrc.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build/
│ ├── build.js
│ ├── loaders/
│ │ └── handlebars-minifier-loader.js
│ ├── webpack.analyser.js
│ ├── webpack.base.js
│ ├── webpack.dev.js
│ ├── webpack.polyfill.js
│ └── webpack.prod.js
├── eruda.d.ts
├── eslint.config.mjs
├── karma.conf.js
├── package.json
├── src/
│ ├── Console/
│ │ ├── Console.js
│ │ └── Console.scss
│ ├── DevTools/
│ │ ├── DevTools.js
│ │ ├── DevTools.scss
│ │ └── Tool.js
│ ├── Elements/
│ │ ├── CssStore.js
│ │ ├── Detail.js
│ │ ├── Elements.js
│ │ ├── Elements.scss
│ │ └── util.js
│ ├── EntryBtn/
│ │ ├── EntryBtn.js
│ │ └── EntryBtn.scss
│ ├── Info/
│ │ ├── Info.js
│ │ ├── Info.scss
│ │ └── defInfo.js
│ ├── Network/
│ │ ├── Detail.js
│ │ ├── Network.js
│ │ ├── Network.scss
│ │ └── util.js
│ ├── Resources/
│ │ ├── Cookie.js
│ │ ├── Resources.js
│ │ ├── Resources.scss
│ │ ├── Storage.js
│ │ └── util.js
│ ├── Settings/
│ │ ├── Settings.js
│ │ └── Settings.scss
│ ├── Snippets/
│ │ ├── Snippets.js
│ │ ├── Snippets.scss
│ │ ├── defSnippets.js
│ │ └── searchText.scss
│ ├── Sources/
│ │ ├── Sources.js
│ │ └── Sources.scss
│ ├── eruda.js
│ ├── index.js
│ ├── lib/
│ │ ├── chobitsu.js
│ │ ├── emitter.js
│ │ ├── empty.js
│ │ ├── evalCss.js
│ │ ├── logger.js
│ │ ├── micromark.js
│ │ ├── themes.js
│ │ └── util.js
│ ├── polyfill.js
│ └── style/
│ ├── icon.css
│ ├── icon.json
│ ├── luna.scss
│ ├── mixin.scss
│ ├── reset.scss
│ ├── style.scss
│ └── variable.scss
└── test/
├── boot.js
├── console.html
├── console.js
├── data.json
├── elements.html
├── elements.js
├── eruda.html
├── eruda.js
├── index.html
├── info.html
├── info.js
├── init.js
├── inline.html
├── manual.html
├── network.html
├── network.js
├── resources.html
├── resources.js
├── settings.html
├── settings.js
├── snippets.html
├── snippets.js
├── sources.html
├── sources.js
├── style.css
└── util.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .eustia.js
================================================
module.exports = {
test: {
library: ['node_modules/eustia-module'],
files: ['test/*.js', 'test/*.html'],
exclude: ['js'],
namespace: 'util',
output: 'test/util.js',
},
}
================================================
FILE: .gitattributes
================================================
* text=auto eol=lf
================================================
FILE: .github/FUNDING.yml
================================================
open_collective: eruda
ko_fi: surunzi
custom: [surunzi.com/wechatpay.html]
================================================
FILE: .github/workflows/main.yml
================================================
name: CI
on:
workflow_dispatch:
push:
branches:
- 'master'
paths:
- 'src/**/*'
- 'test/**/*'
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18.x'
- run: |
npm install -g @liriliri/lsla
npm i
npm run ci
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish to NPM
on:
workflow_dispatch:
release:
types: [created]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18.x'
registry-url: 'https://registry.npmjs.org'
- run: |
npm i -g @liriliri/lsla
npm i
npm run build
- working-directory: dist
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .gitignore
================================================
.idea/
dist/
node_modules/
test/lib/
coverage/
test/playground.html
npm-debug.log
package-lock.json
================================================
FILE: .gitmodules
================================================
[submodule "src/style/icon"]
path = src/style/icon
url = https://github.com/liriliri/icon-share.git
================================================
FILE: .prettierignore
================================================
test/util.js
================================================
FILE: .prettierrc.json
================================================
{
"singleQuote": true,
"tabWidth": 2,
"semi": false
}
================================================
FILE: CHANGELOG.md
================================================
## 3.4.3 (15 Jun 2025)
* fix: redundant code imported
## 3.4.2 (15 Jun 2025)
* fix: elements horizontal scrollbar [#504](https://github.com/liriliri/eruda/issues/504)
## 3.4.1 (10 Nov 2024)
* fix: no copy and delete for shadow root
* fix: fetch remains pending when error occurs
* fix: theme not updated if system theme changed
## 3.4.0 (27 Sep 2024)
* feat: support shadow dom [#158](https://github.com/liriliri/eruda/issues/158)
* fix: quirks mode table rendering [#459](https://github.com/liriliri/eruda/issues/459)
## 3.3.0 (9 Sep 2024)
* feat: add vue devtools plugin
## 3.2.3 (10 AUG 2024)
* fix: WebSocket message base64 encoded [#447](https://github.com/liriliri/eruda/issues/447)
## 3.2.2 (8 AUG 2024)
* chore: update plugin versions
## 3.2.1 (20 JUL 2024)
* fix: touches plugin [#344](https://github.com/liriliri/eruda/issues/344)
## 3.2.0 (16 JUL 2024)
* feat: support inline mode
* feat: allow spaces in plugin name
* fix: some typescript d.ts mistakes
* chore: remove elements set api
* chore: update monitor plugin version
## 3.1.0 (9 JUL 2024)
* feat: add AMOLED theme [#414](https://github.com/liriliri/eruda/pull/414)
* feat: support system preference theme config
* feat: add isDarkTheme, getTheme util
* fix: backers.svg lazy loading [#407](https://github.com/liriliri/eruda/issues/407)
## 3.0.1 (18 JUL 2023)
* fix: can not print string with %o [#336](https://github.com/liriliri/eruda/issues/336)
* fix: mouse event on touch device [#302](https://github.com/liriliri/eruda/issues/302)
* fix: unable to remove snippets [#349](https://github.com/liriliri/eruda/issues/349)
## 3.0.0 (2 Apr 2023)
* feat: replace fps and memory with monitor plugin
* fix: resource stylesheet show failed
* chore: remove licia utils
* chore: separate polyfill
## 2.11.3 (3 Mar 2023)
* fix: scale [#307](https://github.com/liriliri/eruda/issues/307)
## 2.11.2 (28 Jan 2023)
* fix: check safe area error
## 2.11.1 (28 Jan 2023)
* fix: bottom safe area
* fix(console): filter function support
* fix: click event stop propagation [#155](https://github.com/liriliri/eruda/issues/155)
* fix: worker null error [#152](https://github.com/liriliri/eruda/issues/152)
## 2.11.0 (19 Jan 2023)
* feat(network): filter
* feat(info): add backers
* feat(settings): use luna setting
* feat(resources): use luna data grid
* feat(resources): copy storage, cookie
* fix(sources): code not selectable
* fix(console): filter api
## 2.10.0 (24 Dec 2022)
* feat(sources): use luna text viewer
* feat(elements): split mode
* feat(network): split mode
* fix(resources): delete cookie
## 2.9.1 (20 Dec 2022)
* fix(elements): select element using touch events
## 2.9.0 (20 Dec 2022)
* feat(elements): integrate dom viewer
* feat(elements): element crumbs
* feat(elements): copy node and delete node
* feat(network): copy response
* feat(network): toggle recording
* chore: remove dom plugin snippet
## 2.8.3 (13 Dec 2022)
* fix(network): remove data grid ios outline
* chore: update luna console and luna object viewer
## 2.8.2 (12 Dec 2022)
* fix: some variables not reset when destroy
## 2.8.1 (12 Dec 2022)
* fix: remove luna syntax highlighter
## 2.8.0 (11 Dec 2022)
* feat(info): copy
* feat(sources): use luna syntax highlighter
* feat(network): use luna data grid
* feat(network): copy as curl [#220](https://github.com/liriliri/eruda/issues/220)
* fix(network): recognize JSON [#201](https://github.com/liriliri/eruda/issues/201)
* fix: init with shadow dom style error [#195](https://github.com/liriliri/eruda/issues/195)
## 2.7.4 (10 Dec 2022)
* fix: firefox document.body is null error [#293](https://github.com/liriliri/eruda/issues/293)
## 2.7.3 (8 Dec 2022)
* fix: remove tabs horizontal scrollbar [#236](https://github.com/liriliri/eruda/issues/236)
## 2.7.2 (7 Dec 2022)
* fix: luna modal style
## 2.7.1 (7 Dec 2022)
* fix: remove debug log
## 2.7.0 (7 Dec 2022)
* feat: drag to resize
* feat: update icons
* feat: use luna modal to replace browser prompt
## 2.6.2 (3 Dec 2022)
* feat: support android 5.0
* feat(sources): remove code beautify
* fix: code plugin theme
## 2.6.1 (26 Nov 2022)
* fix: dark mode scrollbar style
* fix: unable to load timing plugin
## 2.6.0 (25 Nov 2022)
* feat(console): select and copy
* chore: update luna console
* chore: update chobitsu
## 2.5.0 (9 Jul 2022)
* feat: add ts declaration [#187](https://github.com/liriliri/eruda/pull/187)
* refactor: use luna console
* refactor: use chobitsu for highlighting element
## 2.4.1 (28 Sep 2020)
* fix: remove arrow function [#160](https://github.com/liriliri/eruda/issues/160)
## 2.4.0 (14 Sep 2020)
* feat: default settings [#141](https://github.com/liriliri/eruda/issues/141)
* fix(elements): highlight
* fix(console): blinks frequently as it scroll to the border
* refactor: use chobitsu
## 2.3.3 (3 May 2020)
* fix: unsafe-eval CSP violation [#140](https://github.com/liriliri/eruda/issues/140)
## v2.3.2 (29 Apr 2020)
* fix(console): scroll performance
## v2.3.1 (28 Apr 2020)
* fix(elements): content highlight
## v2.3.0 (28 Apr 2020)
* feat: refresh notification
* fix(console): safari bounce effect
* fix(elements): highlight
## v2.2.2 (17 Apr 2020)
* fix(console): extra info from
* chore: update icons
## v2.2.1 (20 Mar 2020)
* fix: redundant evaluated style
* chore: use [luna-object-viewer](https://github.com/liriliri/luna) for viewing object
## v2.2.0 (9 Feb 2020)
* feat: use dark theme for dark mode
* feat(elements): computed style filter
* feat(resources): storage and cookie filter
* fix(snippet): error loading plugin for local page
* fix(console): unable to clear filter
## v2.1.0 (2 Feb 2020)
* feat: change navigation bar height
* feat: change default transparency to 1
* feat: change loaded plugin position
* feat(console): remove debug filter
* feat(console): improve input style
* feat(console): show filter text
* feat(network): add requests api [#132](https://github.com/liriliri/eruda/issues/132)
## v2.0.2 (9 Jan 2020)
* chore: reduce file size (452kb -> 418kb)
## v2.0.1 (6 Jan 2020)
* chore: update plugins
## v2.0.0 (3 Jan 2020)
* feat: theme support
* feat(console): $x utility
* feat(console): remove useWorker
* feat(sources): indent size configuration
* fix(console): url recognition
* fix(console): log style
* fix(sources): scrolling
* perf(console): large object expansion
* chore: reduce file size (472kb -> 452kb)
## v1.10.3 (8 Nov 2019)
* fix(info): escape location [#127](https://github.com/liriliri/eruda/issues/127)
* chore: update refresh icon
* chore: update timing plugin version
## v1.10.2 (5 Nov 2019)
* fix: must add .default if using require
## v1.10.1 (4 Nov 2019)
* fix(console): error display when js execution disabled
## v1.10.0 (4 Nov 2019)
* chore: updated to babel7, must add .default if using require
* feat(console): multiple console instance
* perf(console): rendering for a large number of logs
## v1.9.2 (1 Nov 2019)
* perf(console): rendering
## v1.9.1 (27 Oct 2019)
* perf(console): asynchronous log render
* perf(console): reduce memory usage, 50% drop
## v1.9.0 (20 Oct 2019)
* feat: add snippet for loading touches plugin
* feat: add fit screen snippet
* fix(console): filter shouldn't affect group
## v1.8.1 (14 Oct 2019)
* fix(network): style [#121](https://github.com/liriliri/eruda/issues/121)
## v1.8.0 (13 Oct 2019)
* feat(network): display optimization
* feat: move http view from sources to network
* fix(console): group object expansion
## v1.7.2 (11 Oct 2019)
* fix(console): blank bottom if js input is disabled
* chore: update eruda-dom version
## v1.7.1 (10 Oct 2019)
* fix: resize
## v1.7.0 (8 Oct 2019)
* feat: resize [#89](https://github.com/liriliri/eruda/issues/89)
* feat(console): replace help button with filter
* feat(console): disable js execution
* feat(console): [utilities api](https://developers.google.cn/web/tools/chrome-devtools/console/utilities)
* fix(console): disable log collapsing for group
* fix(elements): select not working for desktop
## v1.6.3 (1 Oct 2019)
* fix(console): log border style
## v1.6.2 (29 Sep 2019)
* fix: container style affected [#119](https://github.com/liriliri/eruda/issues/119)
* fix(console): log style, line-height should be normal
## v1.6.1 (27 Sep 2019)
* feat(network): catch fetch request headers
* feat(console): timeLog, countReset
* fix(console): clear not working
* fix(console): table
## v1.6.0 (26 Sep 2019)
* feat: console group
* fix: console style, width and height is forbidden
* fix: regexp json view
* chore: update fps and memory plugin version
## v1.5.8 (2 Aug 2019)
* fix: safeStorage undefined [#108](https://github.com/liriliri/eruda/issues/108)
## v1.5.7 (15 Jul 2019)
* Fix iOS max log number
* Disable calling init if already initialized
* Disable worker by default
* Support xhr blob response type [#104](https://github.com/liriliri/eruda/issues/100)
## v1.5.6 (17 Jun 2019)
* Disable log collapse for objects
## v1.5.5 (25 May 2019)
* Fix resources error when cookie has % [#100](https://github.com/liriliri/eruda/issues/100)
* Update dom plugin version
## v1.5.4 (23 Sep 2018)
* Fix network url start with //
* Smaller padding for logs
## v1.5.3 (2 Sep 2018)
* Add load dom plugin snippet
* Disable highlight for invisible elements
* Fix unexpected token \t in JSON
* Add load orientation plugin snippet
## v1.5.2 (23 Aug 2018)
* Fix console show in sources panel
* Fix log merge
* Support getting entryBtn instance
* Update timing plugin version
* Add remove setting api
* Fix safari merge log exception
## v1.5.1 (18 Aug 2018)
* Fix uglifyjs unicode escape [#69](https://github.com/liriliri/eruda/issues/69)
* Update icons, use [iconfont](http://www.iconfont.cn) instead of [icomoon](https://icomoon.io/)
* Show custom request headers [#78](https://github.com/liriliri/eruda/pull/78)
* Add get api to info panel [#83](https://github.com/liriliri/eruda/issues/83)
* Fix responseType json error [#82](https://github.com/liriliri/eruda/issues/82)
* Support console lazy evaluation
## v1.5.0 (19 Jun 2018)
* Use shadow dom to encapsulate css
* Enable sources copy [#71](https://github.com/liriliri/eruda/issues/71)
* Improve **borderAll** style
* Add **position** api [#74](https://github.com/liriliri/eruda/issues/74)
* Fix nav bottom bar wrong position when removed
## v1.4.4 (27 May 2018)
* Improve console line break display
* Add **rmCookie** util
* Add **Load Geolocation Plugin** snippet
* Fix Elements cssRules [#63](https://github.com/liriliri/eruda/issues/63)
* Support console events [#66](https://github.com/liriliri/eruda/issues/66)
* Fix Uc browser console worker [#62](https://github.com/liriliri/eruda/issues/62)
## v1.4.3 (7 Feb 2018)
* Dynamic info content support [#51](https://github.com/liriliri/eruda/issues/51)
* Fix console input covered by error log
* Add elements box model chart
* Fix source code white-space style [#53](https://github.com/liriliri/eruda/issues/53)
* Resources support iframe
* Add **Load Benchmark Plugin** snippet
## v1.4.2 (28 Jan 2018)
* Extract viewportScale util into [eris](https://github.com/liriliri/eris)
* Improve image list view using flex
* Add DevTools display event hooks [#50](https://github.com/liriliri/eruda/issues/50)
## v1.4.1 (13 Jan 2018)
* Update timing plugin version
* Fix viewportScale
* Optimize console performance for big data
* Expose snippets run api
* Delete desktop scrollbar style
* Add code plugin to snippets
## v1.4.0 (7 Jan 2018)
* Remove network timing into external plugin
* Add system info
* Add memory plugin snippet
* Monitor fetch requests [#24](https://github.com/liriliri/eruda/issues/24)
* Reduce json viewer click area
* Use resource timing for image capture
## v1.3.2 (14 Dec 2017)
* Fix restore settings snippet
* Extract *features* into an external plugin
## v1.3.1 (19 Nov 2017)
* Observe elements in resources panel
* Fix performance timing not supported [#40](https://github.com/liriliri/eruda/issues/40)
## v1.3.0 (5 Nov 2017)
* Remove log margin
* Fix css custom properties [#33](https://github.com/liriliri/eruda/issues/33)
* Add version info
* Change icomoon generated font name
* Improve snippets style
* Add *Load Fps Plugin* and *Restore Settings* snippets
* Support navbar color customization
* Support range in settings panel
* Support auto scale [#32](https://github.com/liriliri/eruda/issues/32)
* Improve *Border All* snippet
* Use high resolution time for console time
## v1.2.6 (31 Aug 2017)
* Fix catch global errors
## v1.2.5 (20 Aug 2017)
* Fix cookie URI malformed
* Fix single string argument unescaped
* Update util library and dependencies
* Fix catch event listeners [#31](https://github.com/liriliri/eruda/issues/31)
* Console log scroll automatically only at bottom
* Fix unformatted html tag
## v1.2.4 (1 Jul 2017)
* Fix uncaught promise error [#29](https://github.com/liriliri/eruda/issues/23)
* Fix bad classes [#28](https://github.com/liriliri/eruda/issues/23)
## v1.2.3 (15 May 2017)
* Disable modernizr classes
* Update eustia util
* Fix console resize [#23](https://github.com/liriliri/eruda/issues/23)
* Improve object log
* Use outline for borderAll snippet
## v1.2.2 (11 Mar 2017)
* Fix log url recognition
* Fix error log stack url and style
* Fix table log ouput
* Fix storage initialization [#20](https://github.com/liriliri/eruda/issues/20)
* Update eustia lib
* Elements auto refresh
* Add pc scrollbar style
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2016-present liriliri
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
Eruda
Console for Mobile Browsers.
[![NPM version][npm-image]][npm-url]
[![Build status][ci-image]][ci-url]
[![Test coverage][codecov-image]][codecov-url]
[![Downloads][jsdelivr-image]][jsdelivr-url]
[![License][license-image]][npm-url]
[npm-image]: https://img.shields.io/npm/v/eruda?style=flat-square
[npm-url]: https://npmjs.org/package/eruda
[jsdelivr-image]: https://img.shields.io/jsdelivr/npm/hm/eruda?style=flat-square
[jsdelivr-url]: https://www.jsdelivr.com/package/npm/eruda
[ci-image]: https://img.shields.io/github/actions/workflow/status/liriliri/eruda/main.yml?branch=master&style=flat-square
[ci-url]: https://github.com/liriliri/eruda/actions/workflows/main.yml
[codecov-image]: https://img.shields.io/codecov/c/github/liriliri/eruda?style=flat-square
[codecov-url]: https://codecov.io/github/liriliri/eruda?branch=master
[license-image]: https://img.shields.io/npm/l/eruda?style=flat-square
[donate-image]: https://img.shields.io/badge/$-donate-0070ba.svg?style=flat-square
## Demo

Browse it on your phone: [eruda.liriliri.io](https://eruda.liriliri.io/)
## Install
You can get it on npm.
```bash
npm install eruda --save-dev
```
Add this script to your page.
```html
```
It's also available on [jsDelivr](http://www.jsdelivr.com/projects/eruda) and [cdnjs](https://cdnjs.com/libraries/eruda).
```html
```
For more detailed usage instructions, please read the documentation at [eruda.liriliri.io](https://eruda.liriliri.io/docs/)!
## Related Projects
* [eruda-android](https://github.com/liriliri/eruda-android): Simple webview with eruda loaded automatically.
* [chii](https://github.com/liriliri/chii): Remote debugging tool.
* [chobitsu](https://github.com/liriliri/chobitsu): Chrome devtools protocol JavaScript implementation.
* [licia](https://github.com/liriliri/licia): Utility library used by eruda.
* [luna](https://github.com/liriliri/luna): UI components used by eruda.
* [vivy](https://github.com/liriliri/vivy-docs): Icon image generation.
## Third Party
* [eruda-pixel](https://github.com/Faithree/eruda-pixel): UI pixel restoration tool.
* [eruda-webpack-plugin](https://github.com/huruji/eruda-webpack-plugin): Eruda webpack plugin.
* [eruda-vue-devtools](https://github.com/Zippowxk/vue-devtools-plugin): Eruda Vue-devtools plugin.
## Backers
## Contribution
Read [Contributing Guide](https://eruda.liriliri.io/docs/contributing.html) for development setup instructions.
================================================
FILE: build/build.js
================================================
const path = require('path')
const fs = require('licia/fs')
const pkg = require('../package.json')
delete pkg.scripts
delete pkg.devDependencies
fs.writeFile(
path.resolve(__dirname, '../dist/package.json'),
JSON.stringify(pkg, null, 2),
'utf8'
)
================================================
FILE: build/loaders/handlebars-minifier-loader.js
================================================
module.exports = function (src) {
return src.replace(/"loc":\{"start":\{"line":\d+,"column":\d+},"end":\{"line":\d+,"column":\d+\}\}/g, '')
}
================================================
FILE: build/webpack.analyser.js
================================================
const BundleAnalyzerPlugin =
require('webpack-bundle-analyzer').BundleAnalyzerPlugin
exports = require('./webpack.prod')
exports.plugins.push(new BundleAnalyzerPlugin())
module.exports = exports
================================================
FILE: build/webpack.base.js
================================================
const autoprefixer = require('autoprefixer')
const prefixer = require('postcss-prefixer')
const clean = require('postcss-clean')
const webpack = require('webpack')
const pkg = require('../package.json')
const path = require('path')
const ESLintPlugin = require('eslint-webpack-plugin')
process.traceDeprecation = true
const banner = pkg.name + ' v' + pkg.version + ' ' + pkg.homepage
const postcssLoader = {
loader: 'postcss-loader',
options: {
plugins: [
prefixer({
prefix: '_',
ignore: [/luna-*/],
}),
autoprefixer,
clean(),
],
},
}
const rawLoader = {
loader: 'raw-loader',
options: {
esModule: false,
},
}
module.exports = {
entry: './src/index',
resolve: {
symlinks: false,
alias: {
axios: path.resolve(__dirname, '../src/lib/empty.js'),
micromark: path.resolve(__dirname, '../src/lib/micromark.js'),
},
},
devServer: {
static: {
directory: path.join(__dirname, '../test'),
},
port: 8080,
},
output: {
path: path.resolve(__dirname, '../dist'),
publicPath: '/assets/',
library: 'eruda',
libraryTarget: 'umd',
},
module: {
rules: [
{
test: /\.js$/,
include: [
path.resolve(__dirname, '../src'),
path.resolve(__dirname, '../node_modules/luna-console'),
path.resolve(__dirname, '../node_modules/luna-modal'),
path.resolve(__dirname, '../node_modules/luna-tab'),
path.resolve(__dirname, '../node_modules/luna-data-grid'),
path.resolve(__dirname, '../node_modules/luna-object-viewer'),
path.resolve(__dirname, '../node_modules/luna-dom-viewer'),
path.resolve(__dirname, '../node_modules/luna-text-viewer'),
path.resolve(__dirname, '../node_modules/luna-setting'),
path.resolve(__dirname, '../node_modules/luna-box-model'),
path.resolve(__dirname, '../node_modules/luna-notification'),
],
use: [
{
loader: 'babel-loader',
options: {
sourceType: 'unambiguous',
presets: ['@babel/preset-env'],
plugins: [
'@babel/plugin-transform-runtime',
'@babel/plugin-proposal-class-properties',
],
},
},
],
},
{
test: /\.scss$/,
use: [
'css-loader',
postcssLoader,
{ loader: 'sass-loader', options: { api: 'modern' } },
],
},
{
test: /\.css$/,
exclude: /luna-dom-highlighter/,
use: ['css-loader', postcssLoader],
},
{
test: /luna-dom-highlighter\.css$/,
use: [rawLoader],
},
],
},
plugins: [
new webpack.BannerPlugin(banner),
new webpack.DefinePlugin({
VERSION: '"' + pkg.version + '"',
}),
new ESLintPlugin(),
],
}
================================================
FILE: build/webpack.dev.js
================================================
const webpack = require('webpack')
exports = require('./webpack.base')
exports.mode = 'development'
exports.output.filename = 'eruda.js'
exports.devtool = 'source-map'
exports.plugins = exports.plugins.concat([
new webpack.DefinePlugin({
ENV: '"development"',
}),
])
module.exports = exports
================================================
FILE: build/webpack.polyfill.js
================================================
const path = require('path')
module.exports = {
mode: 'production',
entry: './src/polyfill',
output: {
path: path.resolve(__dirname, '../dist'),
filename: 'eruda-polyfill.js',
},
}
================================================
FILE: build/webpack.prod.js
================================================
const webpack = require('webpack')
const TerserPlugin = require('terser-webpack-plugin')
exports = require('./webpack.base')
exports.mode = 'production'
exports.output.filename = 'eruda.js'
exports.devtool = 'source-map'
exports.plugins = exports.plugins.concat([
new webpack.DefinePlugin({
ENV: '"production"',
}),
])
exports.optimization = {
minimize: true,
minimizer: [
new TerserPlugin({
extractComments: false,
}),
],
}
module.exports = exports
================================================
FILE: eruda.d.ts
================================================
/**
* Type definitions for Eruda
* @see https://github.com/liriliri/eruda
*/
declare module 'eruda' {
export interface InitDefaults {
/**
* Transparency, 0 to 1
*/
transparency?: number
/**
* Display size, 0 to 100
*/
displaySize?: number
/**
* Theme, defaults to Light or Dark in dark mode
*/
theme?: string
}
export interface InitOptions {
/**
* Container element. If not set, it will append an element directly under html root element
*/
container?: HTMLElement
/**
* Choose which default tools you want, by default all will be added
*/
tool?: string[]
/**
* Auto scale eruda for different viewport settings
*/
autoScale?: boolean
/**
* Use shadow dom for css encapsulation
*/
useShadowDom?: boolean
/**
* Enable inline mode
*/
inline?: boolean
/**
* Default settings
*/
defaults?: InitDefaults
}
export interface Position {
x: number
y: number
}
type AnyFn = (...args: any[]) => any
export interface Emitter {
on(event: string, listener: AnyFn): Emitter
off(event: string, listener: AnyFn): Emitter
once(event: string, listener: AnyFn): Emitter
emit(event: string, ...args: any[]): Emitter
removeAllListeners(event?: string): Emitter
}
/**
* Eruda Plugin
* @see https://eruda.liriliri.io/docs/plugin.html
*/
export interface Tool {
/**
* Every plugin must have a unique name, which will be shown as the tab name on the top.
*/
name: string
/**
* Called when plugin is added, and a document element used to display content is passed in.
* The element is wrapped as a jQuery like object, provided by the licia utility library.
*/
init(el: unknown): void
/**
* Called when switch to the panel. Usually all you need to do is to show the container element.
*/
show(): Tool | undefined
/**
* Called when switch to other panel. You should at least hide the container element here.
*/
hide(): Tool | undefined
/**
* Called when plugin is removed using `eruda.remove('plugin name')`.
*/
destroy(): void
}
export interface ToolConstructor {
new (): Tool
readonly prototype: Tool
extend(tool: Tool): ToolConstructor
}
export interface ConsoleConfig {
/**
* Asynchronous rendering
*/
asyncRender?: boolean
/**
* Enable JavaScript execution
*/
jsExecution?: boolean
/**
* Catch global errors
*/
catchGlobalErr?: boolean
/**
* Override console
*/
overrideConsole?: boolean
/**
* Display extra information
*/
displayExtraInfo?: boolean
/**
* Display unenumerable properties
*/
displayUnenumerable?: boolean
/**
* Access getter value
*/
displayGetterVal?: boolean
/**
* Stringify object when clicked
*/
lazyEvaluation?: boolean
/**
* Auto display if error occurs
*/
displayIfErr?: boolean
/**
* Max log number
*/
maxLogNum?: string
}
export interface Log {
type: string
}
export interface ErudaConsole extends Tool, Console {
config: {
set(name: K, value: ConsoleConfig[K]): void
}
/**
* Custom filter
*/
filter(pattern: string | RegExp | ((log: Log) => boolean)): void
/**
* Html string
*/
html(htmlStr: string): void
}
export interface ErudaConsoleConstructor {
new (): ErudaConsole
readonly prototype: ErudaConsole
}
export interface ElementsConfig {
/**
* Catch Event Listeners
*/
overrideEventTarget?: boolean
/**
* Auto Refresh
*/
observeElement?: boolean
}
export interface Elements extends Tool {
config: {
set(
name: K,
value: ElementsConfig[K]
): void
}
/**
* Element to display
*/
select(el: HTMLElement): void
}
export interface ElementsConstructor {
new (): Elements
readonly prototype: Elements
}
export interface Network extends Tool {
/**
* Clear requests
*/
clear(): void
/**
* Get request data
*/
requests(): object[]
}
export interface NetworkConstructor {
new (): Network
readonly prototype: Network
}
export interface ResourcesConfig {
/**
* Hide Eruda Setting
*/
hideErudaSetting?: boolean
/**
* Auto Refresh Elements
*/
observeElement?: boolean
}
export interface Resources extends Tool {
config: {
set(
name: K,
value: ResourcesConfig[K]
): void
}
}
export interface ResourcesConstructor {
new (): Resources
readonly prototype: Resources
}
export interface SourcesConfig {
/**
* Show Line Numbers
*/
showLineNum?: boolean
/**
* Beautify Code
*/
formatCode?: boolean
/**
* Indent Size
*/
indentSize?: string
}
export interface Sources extends Tool {
config: {
set(name: K, value: SourcesConfig[K]): void
}
}
export interface SourcesConstructor {
new (): Sources
readonly prototype: Sources
}
export interface InfoItem {
name: string
val: string
}
export interface Info extends Tool {
/**
* Clear infos
*/
clear(): void
/**
* Add info
*/
add(name: string, content: string | (() => void)): void
/**
* Get info or infos
*/
get(): InfoItem[]
get(name: string): string
/**
* Remove specified info
*/
remove(name: string): void
}
export interface InfoConstructor {
new (): Info
readonly prototype: Info
}
export interface Snippets extends Tool {
/**
* Clear snippets
*/
clear(): void
/**
* Add snippet
* @param name Snippet name
* @param fn Function to be triggered
* @param desc Snippet description
*/
add(name: string, fn: Function, desc: string): void
/**
* Remove specified snippet
* @param name Snippet name
*/
remove(name: string): void
/**
* Run specified snippet
* @param name Snippet name
*/
run(name: string): void
}
export interface SnippetsConstructor {
new (): Snippets
readonly prototype: Snippets
}
export interface SettingsRangeOptions {
min?: number
max?: number
step?: number
}
export interface Settings extends Tool {
/**
* Clear settings
*/
clear(): void
/**
* Remove setting
* @param cfg Config object
* @param name Option name
*/
remove(cfj: object, name: string): void
/**
* Add text
*/
text(str: string): void
/**
* Add switch to toggle a boolean value
* @param cfg Config object created by util.createCfg
* @param name Option name
* @param desc Option description
*/
switch(cfg: object, name: string, desc: string): void
/**
* Add select to select a number of string values
* @param cfg Config object
* @param name Option name
* @param desc Option description
* @param values Array of strings to select
*/
select(cfg: object, name: string, desc: string, values: string[]): void
/**
* Add range to input a number
* @param cfg Config object
* @param name Option name
* @param desc Option description
* @param options Min, max, step
*/
range(
cfg: object,
name: string,
desc: string,
options?: SettingsRangeOptions
): void
/**
* Add a separator
*/
separator(): void
}
export interface SettingsConstructor {
new (): Settings
readonly prototype: Settings
}
export interface EntryBtn extends Emitter {
show(): void
hide(): void
getPos(): Position
setPos(pos: Position): void
destroy(): void
}
export interface EntryBtnConstructor {
new (): EntryBtn
readonly prototype: EntryBtn
}
export interface DevTools extends Emitter {
show(): DevTools
hide(): DevTools
toggle(): void
add(tool: Tool | object): DevTools
remove(name: string): DevTools
removeAll(): DevTools
get(name: string): InstanceType | undefined
showTool(name: string): DevTools
initCfg(settings: Settings): void
notify(content: string, options: object): void
destroy(): void
}
export interface DevToolsConstructor {
new (): DevTools
readonly prototype: DevTools
}
/**
* Eruda Util
* @see https://eruda.liriliri.io/docs/plugin.html#utility
*/
export interface Util {
evalCss(css: string): HTMLStyleElement
isErudaEl(val: any): boolean
isDarkTheme(theme?: string): boolean
getTheme(): string
}
interface IToolNameMap {
console: InstanceType
elements: InstanceType
info: InstanceType
network: InstanceType
resources: InstanceType
settings: InstanceType
snippets: InstanceType
sources: InstanceType
entryBtn: InstanceType
}
/**
* Eruda APIs
* @see https://eruda.liriliri.io/docs/api.html
*/
export interface ErudaApis {
/**
* Initialize eruda.
*/
init(options?: InitOptions): void
/**
* Destory eruda.
* Note: You can call `init` method again after destruction.
*/
destroy(): void
/**
* Set or get scale.
*/
scale(): number
scale(s: number): Eruda
/**
* Set or get entry button position.
* It will not take effect if given pos is out of range.
*/
position(): Position
position(p: Position): Eruda
/**
* Get tool, eg. console, elements panels.
*/
get(name: K): IToolNameMap[K]
get(name: string): InstanceType | undefined
get(): InstanceType
/**
* Add tool.
*/
add(
tool: InstanceType | ((eruda: Eruda) => InstanceType)
): Eruda | undefined
/**
* Remove tool.
*/
remove(name: string): Eruda | undefined
/**
* Show eruda panel.
*/
show(name?: string): Eruda | undefined
/**
* Hide eruda panel.
*/
hide(): Eruda | undefined
}
export interface Eruda extends ErudaApis {
/**
* Display console logs. Implementation detail follows the console api spec.
*/
Console: ErudaConsoleConstructor
/**
* Check dom element status.
*/
Elements: ElementsConstructor
/**
* Display special information, could be used for displaying user info to track user logs.
* By default, page url and browser user agent is shown.
*/
Info: InfoConstructor
/**
* Display requests.
*/
Network: NetworkConstructor
/**
* LocalStorage, sessionStorage, cookies, scripts, styleSheets and images.
*/
Resources: ResourcesConstructor
/**
* Customization for all tools.
*/
Settings: SettingsConstructor
/**
* Allow you to register small functions that can be triggered multiple times.
*/
Snippets: SnippetsConstructor
/**
* View object, html, js, and css.
*/
Sources: SourcesConstructor
/**
* Eruda Tool
*/
Tool: ToolConstructor
/**
* Eruda Util
*/
util: Util
/**
* Eruda version
*/
readonly version: string
}
const eruda: Eruda
export default eruda
}
================================================
FILE: eslint.config.mjs
================================================
import babelEslintParser from '@babel/eslint-parser'
import eslintJs from '@eslint/js'
import globals from 'globals'
export default [
eslintJs.configs.recommended,
{
languageOptions: {
parser: babelEslintParser,
parserOptions: {
requireConfigFile: false,
babelOptions: {
babelrc: false,
configFile: false,
},
},
globals: {
...globals.builtin,
...globals.browser,
...globals.commonjs,
VERSION: true,
ENV: true,
},
},
rules: {
quotes: ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
'prefer-const': 2,
},
},
{files: ['build/**/*.js'], languageOptions:{globals: {...globals.node}}},
{
ignores: ['test','dist','coverage'],
}
]
================================================
FILE: karma.conf.js
================================================
const webpackCfg = require('./build/webpack.dev')
webpackCfg.devtool = 'inline-source-map'
webpackCfg.module.rules.push({
test: /\.js$/,
exclude: /node_modules|lib\/util\.js/,
loader: '@jsdevtools/coverage-istanbul-loader',
enforce: 'post',
options: {
esModules: true,
},
})
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jquery-1.8.3'],
files: [
'src/index.js',
'test/init.js',
'node_modules/jasmine-core/lib/jasmine-core/jasmine.js',
'node_modules/karma-jasmine/lib/boot.js',
'node_modules/karma-jasmine/lib/adapter.js',
'node_modules/jasmine-jquery/lib/jasmine-jquery.js',
'test/util.js',
'test/console.js',
'test/elements.js',
'test/info.js',
'test/network.js',
'test/resources.js',
'test/snippets.js',
'test/sources.js',
'test/settings.js',
'test/eruda.js',
],
plugins: [
'karma-jasmine',
'karma-jquery',
'karma-chrome-launcher',
'karma-webpack',
'karma-sourcemap-loader',
'karma-coverage-istanbul-reporter',
],
webpackServer: {
noInfo: true,
},
preprocessors: {
'src/index.js': ['webpack', 'sourcemap'],
},
webpack: webpackCfg,
coverageIstanbulReporter: {
reports: ['html', 'lcovonly', 'text', 'text-summary'],
},
reporters: ['progress', 'coverage-istanbul'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
browsers: ['ChromeHeadless'],
singleRun: true,
concurrency: Infinity,
})
}
================================================
FILE: package.json
================================================
{
"name": "eruda",
"version": "3.4.3",
"description": "Console for Mobile Browsers",
"main": "eruda.js",
"browserslist": [
"since 2015",
"not dead"
],
"scripts": {
"ci": "npm run lint && npm run test && npm run build && npm run es5",
"build": "lsla shx rm -rf dist && webpack --config build/webpack.prod.js && webpack --config build/webpack.polyfill.js && node build/build && lsla shx cp README.md eruda.d.ts dist",
"build:analyser": "webpack --config build/webpack.analyser.js",
"dev": "webpack-dev-server --config build/webpack.dev.js --host 0.0.0.0",
"test": "karma start",
"format": "lsla prettier \"*.{js,ts}\" \"src/**/*.{js,scss,css,json}\" \"build/*.js\" \"test/*.{js,html}\" --write",
"lint": "eslint .",
"lint:fix": "npm run lint -- --fix",
"es5": "es-check es5 dist/eruda.js dist/eruda-polyfill.js",
"setup": "lsla shx mkdir -p test/lib && lsla shx cp node_modules/jasmine-core/lib/jasmine-core/{jasmine.css,jasmine.js,jasmine-html.js,boot.js} test/lib && lsla shx cp node_modules/jasmine-jquery/lib/jasmine-jquery.js test/lib && lsla shx cp node_modules/jquery/dist/jquery.js test/lib",
"genIcon": "lsla genIcon --input src/style/ --output src/style/icon.css --name eruda-icon --source src/style/icon/ && lsla prettier src/**/*.css --write"
},
"repository": {
"type": "git",
"url": "git+https://github.com/liriliri/eruda.git"
},
"keywords": [
"console",
"mobile",
"debug"
],
"author": "redhoodsu",
"license": "MIT",
"bugs": {
"url": "https://github.com/liriliri/eruda/issues"
},
"homepage": "https://eruda.liriliri.io/",
"devDependencies": {
"@babel/core": "^7.18.6",
"@babel/eslint-parser": "^7.26.10",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-transform-runtime": "^7.18.6",
"@babel/preset-env": "^7.18.6",
"@babel/runtime": "^7.18.6",
"@eslint/js": "^9.22.0",
"@jsdevtools/coverage-istanbul-loader": "^3.0.5",
"autoprefixer": "^9.7.4",
"babel-loader": "^8.2.5",
"chobitsu": "^1.8.4",
"core-js": "^3.37.1",
"css-loader": "^3.4.2",
"es-check": "^6.2.1",
"eslint": "^9.22.0",
"eslint-webpack-plugin": "^5.0.0",
"globals": "^16.0.0",
"jasmine-core": "^2.99.1",
"jasmine-jquery": "^2.1.1",
"jquery": "^3.4.1",
"karma": "^6.4.0",
"karma-chrome-launcher": "^3.1.0",
"karma-coverage-istanbul-reporter": "^2.1.1",
"karma-jasmine": "^1.1.2",
"karma-jquery": "^0.2.4",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^5.0.0",
"licia": "^1.44.0",
"luna-box-model": "^1.0.1",
"luna-console": "^1.3.6",
"luna-data-grid": "^1.6.4",
"luna-dom-viewer": "^1.8.3",
"luna-modal": "^1.3.1",
"luna-notification": "^0.3.3",
"luna-object-viewer": "^0.3.2",
"luna-setting": "^2.0.2",
"luna-tab": "^0.4.3",
"luna-text-viewer": "^0.2.1",
"postcss-clean": "^1.2.2",
"postcss-loader": "^3.0.0",
"postcss-prefixer": "^2.1.3",
"raw-loader": "^4.0.2",
"sass": "^1.77.6",
"sass-loader": "^14.2.1",
"webpack": "^5.92.1",
"webpack-bundle-analyzer": "^4.7.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4"
}
}
================================================
FILE: src/Console/Console.js
================================================
import Tool from '../DevTools/Tool'
import noop from 'licia/noop'
import $ from 'licia/$'
import toStr from 'licia/toStr'
import isFn from 'licia/isFn'
import Emitter from 'licia/Emitter'
import isStr from 'licia/isStr'
import isRegExp from 'licia/isRegExp'
import uncaught from 'licia/uncaught'
import trim from 'licia/trim'
import upperFirst from 'licia/upperFirst'
import isHidden from 'licia/isHidden'
import isNull from 'licia/isNull'
import isArr from 'licia/isArr'
import extend from 'licia/extend'
import evalCss from '../lib/evalCss'
import Settings from '../Settings/Settings'
import LunaConsole from 'luna-console'
import LunaModal from 'luna-modal'
import { classPrefix as c } from '../lib/util'
uncaught.start()
export default class Console extends Tool {
constructor({ name = 'console' } = {}) {
super()
Emitter.mixin(this)
this.name = name
this._selectedLog = null
}
init($el, container) {
super.init($el)
this._container = container
this._appendTpl()
this._initCfg()
this._initLogger()
this._exposeLogger()
this._bindEvent()
}
show() {
super.show()
this._handleShow()
}
overrideConsole() {
const origConsole = (this._origConsole = {})
const winConsole = window.console
CONSOLE_METHOD.forEach((name) => {
let origin = (origConsole[name] = noop)
if (winConsole[name]) {
origin = origConsole[name] = winConsole[name].bind(winConsole)
}
winConsole[name] = (...args) => {
this[name](...args)
origin(...args)
}
})
return this
}
setGlobal(name, val) {
this._logger.setGlobal(name, val)
}
restoreConsole() {
if (!this._origConsole) return this
CONSOLE_METHOD.forEach(
(name) => (window.console[name] = this._origConsole[name])
)
delete this._origConsole
return this
}
catchGlobalErr() {
uncaught.addListener(this._handleErr)
return this
}
ignoreGlobalErr() {
uncaught.rmListener(this._handleErr)
return this
}
filter(filter) {
const $filterText = this._$filterText
const logger = this._logger
if (isStr(filter)) {
$filterText.text(filter)
logger.setOption('filter', trim(filter))
} else if (isRegExp(filter)) {
$filterText.text(toStr(filter))
logger.setOption('filter', filter)
} else if (isFn(filter)) {
$filterText.text('ƒ')
logger.setOption('filter', filter)
}
}
destroy() {
this._logger.destroy()
super.destroy()
this._container.off('show', this._handleShow)
if (this._style) {
evalCss.remove(this._style)
}
this.ignoreGlobalErr()
this.restoreConsole()
this._rmCfg()
}
_handleShow = () => {
if (isHidden(this._$el.get(0))) return
this._logger.renderViewport()
}
_handleErr = (err) => {
this._logger.error(err)
}
_enableJsExecution(enabled) {
const $el = this._$el
const $jsInput = $el.find(c('.js-input'))
if (enabled) {
$jsInput.show()
$el.rmClass(c('js-input-hidden'))
} else {
$jsInput.hide()
$el.addClass(c('js-input-hidden'))
}
}
_appendTpl() {
const $el = this._$el
this._style = evalCss(require('./Console.scss'))
$el.append(
c(`
All
Info
Warning
Error
`)
)
const _$inputContainer = $el.find(c('.js-input'))
const _$input = _$inputContainer.find('textarea')
const _$inputBtns = _$inputContainer.find(c('.buttons'))
extend(this, {
_$control: $el.find(c('.control')),
_$logs: $el.find(c('.logs-container')),
_$inputContainer,
_$input,
_$inputBtns,
_$filterText: $el.find(c('.filter-text')),
})
}
_initLogger() {
const cfg = this.config
let maxLogNum = cfg.get('maxLogNum')
maxLogNum = maxLogNum === 'infinite' ? 0 : +maxLogNum
const $level = this._$control.find(c('.level'))
const logger = new LunaConsole(this._$logs.get(0), {
asyncRender: cfg.get('asyncRender'),
maxNum: maxLogNum,
showHeader: cfg.get('displayExtraInfo'),
unenumerable: cfg.get('displayUnenumerable'),
accessGetter: cfg.get('displayGetterVal'),
lazyEvaluation: cfg.get('lazyEvaluation'),
})
logger.on('optionChange', (name, val) => {
switch (name) {
case 'level':
$level.each(function () {
const $this = $(this)
const level = $this.data('level')
const isMatch = level === val || (level === 'all' && isArr(val))
$this[isMatch ? 'addClass' : 'rmClass'](c('active'))
})
break
}
})
if (cfg.get('overrideConsole')) this.overrideConsole()
this._logger = logger
}
_exposeLogger() {
const logger = this._logger
const methods = ['html'].concat(CONSOLE_METHOD)
methods.forEach(
(name) =>
(this[name] = (...args) => {
logger[name](...args)
this.emit(name, ...args)
return this
})
)
}
_bindEvent() {
const container = this._container
const $input = this._$input
const $inputBtns = this._$inputBtns
const $control = this._$control
const logger = this._logger
const config = this.config
$control
.on('click', c('.clear-console'), () => logger.clear(true))
.on('click', c('.level'), function () {
let level = $(this).data('level')
if (level === 'all') {
level = ['verbose', 'info', 'warning', 'error']
}
logger.setOption('level', level)
})
.on('click', c('.filter'), () => {
LunaModal.prompt('Filter').then((filter) => {
if (isNull(filter)) return
this.filter(filter)
})
})
.on('click', c('.copy'), () => {
this._selectedLog.copy()
container.notify('Copied', { icon: 'success' })
})
$inputBtns
.on('click', c('.cancel'), () => this._hideInput())
.on('click', c('.execute'), () => {
const jsInput = $input.val().trim()
if (jsInput === '') return
logger.evaluate(jsInput)
$input.val('').get(0).blur()
this._hideInput()
})
$input.on('focusin', () => this._showInput())
logger.on('insert', (log) => {
const autoShow = log.type === 'error' && config.get('displayIfErr')
if (autoShow) container.showTool('console').show()
})
logger.on('select', (log) => {
this._selectedLog = log
$control.find(c('.icon-copy')).rmClass(c('icon-disabled'))
})
logger.on('deselect', () => {
this._selectedLog = null
$control.find(c('.icon-copy')).addClass(c('icon-disabled'))
})
container.on('show', this._handleShow)
}
_hideInput() {
this._$inputContainer.rmClass(c('active'))
this._$inputBtns.css('display', 'none')
}
_showInput() {
this._$inputContainer.addClass(c('active'))
this._$inputBtns.css('display', 'flex')
}
_rmCfg() {
const cfg = this.config
const settings = this._container.get('settings')
if (!settings) return
settings
.remove(cfg, 'asyncRender')
.remove(cfg, 'jsExecution')
.remove(cfg, 'catchGlobalErr')
.remove(cfg, 'overrideConsole')
.remove(cfg, 'displayExtraInfo')
.remove(cfg, 'displayUnenumerable')
.remove(cfg, 'displayGetterVal')
.remove(cfg, 'lazyEvaluation')
.remove(cfg, 'displayIfErr')
.remove(cfg, 'maxLogNum')
.remove(upperFirst(this.name))
}
_initCfg() {
const container = this._container
const cfg = (this.config = Settings.createCfg(this.name, {
asyncRender: true,
catchGlobalErr: true,
jsExecution: true,
overrideConsole: true,
displayExtraInfo: false,
displayUnenumerable: true,
displayGetterVal: true,
lazyEvaluation: true,
displayIfErr: false,
maxLogNum: 'infinite',
}))
this._enableJsExecution(cfg.get('jsExecution'))
if (cfg.get('catchGlobalErr')) this.catchGlobalErr()
cfg.on('change', (key, val) => {
const logger = this._logger
switch (key) {
case 'asyncRender':
return logger.setOption('asyncRender', val)
case 'jsExecution':
return this._enableJsExecution(val)
case 'catchGlobalErr':
return val ? this.catchGlobalErr() : this.ignoreGlobalErr()
case 'overrideConsole':
return val ? this.overrideConsole() : this.restoreConsole()
case 'maxLogNum':
return logger.setOption('maxNum', val === 'infinite' ? 0 : +val)
case 'displayExtraInfo':
return logger.setOption('showHeader', val)
case 'displayUnenumerable':
return logger.setOption('unenumerable', val)
case 'displayGetterVal':
return logger.setOption('accessGetter', val)
case 'lazyEvaluation':
return logger.setOption('lazyEvaluation', val)
}
})
const settings = container.get('settings')
if (!settings) return
settings
.text(upperFirst(this.name))
.switch(cfg, 'asyncRender', 'Asynchronous Rendering')
.switch(cfg, 'jsExecution', 'Enable JavaScript Execution')
.switch(cfg, 'catchGlobalErr', 'Catch Global Errors')
.switch(cfg, 'overrideConsole', 'Override Console')
.switch(cfg, 'displayIfErr', 'Auto Display If Error Occurs')
.switch(cfg, 'displayExtraInfo', 'Display Extra Information')
.switch(cfg, 'displayUnenumerable', 'Display Unenumerable Properties')
.switch(cfg, 'displayGetterVal', 'Access Getter Value')
.switch(cfg, 'lazyEvaluation', 'Lazy Evaluation')
.select(cfg, 'maxLogNum', 'Max Log Number', [
'infinite',
'250',
'125',
'100',
'50',
'10',
])
.separator()
}
}
const CONSOLE_METHOD = [
'log',
'error',
'info',
'warn',
'dir',
'time',
'timeLog',
'timeEnd',
'clear',
'table',
'assert',
'count',
'countReset',
'debug',
'group',
'groupCollapsed',
'groupEnd',
]
================================================
FILE: src/Console/Console.scss
================================================
@use '../style/variable' as *;
@use '../style/mixin' as *;
#console {
padding-top: 40px;
padding-bottom: 24px;
width: 100%;
height: 100%;
&.js-input-hidden {
padding-bottom: 0;
}
.control {
padding: 10px 10px 10px 35px;
@include control();
.icon-clear {
padding-right: 0px;
left: 0;
}
.icon-copy {
right: 0;
}
.icon-filter {
right: 23px;
}
.level {
cursor: pointer;
font-size: $font-size-s;
height: 20px;
display: inline-block;
margin: 0 2px;
padding: 0 4px;
line-height: 20px;
transition: background-color $anim-duration, color $anim-duration;
&.active {
background: var(--highlight);
color: var(--select-foreground);
}
}
.filter-text {
white-space: nowrap;
position: absolute;
line-height: 20px;
max-width: 80px;
overflow: hidden;
right: 55px;
font-size: $font-size;
text-overflow: ellipsis;
}
}
.js-input {
pointer-events: none;
position: absolute;
z-index: 100;
left: 0;
bottom: 0;
width: 100%;
border-top: 1px solid var(--border);
height: 24px;
.icon-right {
line-height: 23px;
color: var(--accent);
position: absolute;
left: 10px;
top: 0;
z-index: 10;
}
&.active {
height: 100%;
padding-top: 40px;
padding-bottom: 40px;
border-top: none;
.icon-right {
display: none;
}
textarea {
overflow: auto;
padding-left: 10px;
}
}
.buttons {
display: none;
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 40px;
color: var(--primary);
background: var(--darker-background);
font-size: $font-size-s;
border-top: 1px solid var(--border);
.button {
pointer-events: all;
cursor: pointer;
flex: 1;
text-align: center;
border-right: 1px solid var(--border);
height: 40px;
line-height: 40px;
transition: background-color $anim-duration, color $anim-duration;
&:last-child {
border-right: none;
}
&:active {
color: var(--select-foreground);
background: var(--highlight);
}
}
}
textarea {
overflow: hidden;
pointer-events: all;
padding: 3px 10px;
padding-left: 25px;
outline: none;
border: none;
font-size: $font-size;
width: 100%;
height: 100%;
user-select: text;
resize: none;
color: var(--primary);
background: var(--background);
}
}
}
.safe-area #console {
@include safe-area(padding-bottom, 24px);
&.js-input-hidden {
padding-bottom: 0;
}
.js-input {
@include safe-area(height, 24px);
&.active {
height: 100%;
@include safe-area(padding-bottom, 40px);
}
.buttons {
@include safe-area(height, 40px);
.button {
@include safe-area(height, 40px);
}
}
}
}
================================================
FILE: src/DevTools/DevTools.js
================================================
import logger from '../lib/logger'
import Tool from './Tool'
import Settings from '../Settings/Settings'
import Emitter from 'licia/Emitter'
import defaults from 'licia/defaults'
import keys from 'licia/keys'
import last from 'licia/last'
import each from 'licia/each'
import isNum from 'licia/isNum'
import nextTick from 'licia/nextTick'
import $ from 'licia/$'
import toNum from 'licia/toNum'
import extend from 'licia/extend'
import isStr from 'licia/isStr'
import theme from 'licia/theme'
import upperFirst from 'licia/upperFirst'
import startWith from 'licia/startWith'
import ready from 'licia/ready'
import pointerEvent from 'licia/pointerEvent'
import evalCss from '../lib/evalCss'
import emitter from '../lib/emitter'
import { isDarkTheme } from '../lib/themes'
import LunaNotification from 'luna-notification'
import LunaModal from 'luna-modal'
import LunaTab from 'luna-tab'
import {
classPrefix as c,
eventClient,
hasSafeArea,
safeStorage,
} from '../lib/util'
export default class DevTools extends Emitter {
constructor($container, { defaults = {}, inline = false } = {}) {
super()
this._defCfg = extend(
{
transparency: 1,
displaySize: 80,
theme: 'System preference',
},
defaults
)
this._style = evalCss(require('./DevTools.scss'))
this.$container = $container
this._isShow = false
this._opacity = 1
this._tools = {}
this._isResizing = false
this._resizeTimer = null
this._resizeStartY = 0
this._resizeStartSize = 0
this._inline = inline
this._initTpl()
this._initTab()
this._initNotification()
this._initModal()
ready(() => this._checkSafeArea())
this._bindEvent()
}
show() {
this._isShow = true
this._$el.show()
this._tab.updateSlider()
// Need a delay after show to enable transition effect.
setTimeout(() => {
this._$el.css('opacity', this._opacity)
}, 50)
this.emit('show')
return this
}
hide() {
if (this._inline) {
return
}
this._isShow = false
this.emit('hide')
this._$el.css({ opacity: 0 })
setTimeout(() => this._$el.hide(), 300)
return this
}
toggle() {
return this._isShow ? this.hide() : this.show()
}
add(tool) {
const tab = this._tab
if (!(tool instanceof Tool)) {
const { init, show, hide, destroy } = new Tool()
defaults(tool, { init, show, hide, destroy })
}
const name = tool.name
if (!name) {
return logger.error('You must specify a name for a tool')
}
if (this._tools[name]) {
return logger.warn(`Tool ${name} already exists`)
}
const id = name.replace(/\s+/g, '-')
this._$tools.prepend(`
`)
tool.init(this._$tools.find(`.${c(id)}.${c('tool')}`), this)
tool.active = false
this._tools[name] = tool
if (name === 'settings') {
tab.append({
id: name,
title: name,
})
} else {
tab.insert(tab.length - 1, {
id: name,
title: name,
})
}
return this
}
remove(name) {
const tools = this._tools
if (!tools[name]) return logger.warn(`Tool ${name} doesn't exist`)
this._tab.remove(name)
const tool = tools[name]
delete tools[name]
if (tool.active) {
const toolKeys = keys(tools)
if (toolKeys.length > 0) this.showTool(tools[last(toolKeys)].name)
}
tool.destroy()
return this
}
removeAll() {
each(this._tools, (tool) => this.remove(tool.name))
return this
}
get(name) {
const tool = this._tools[name]
if (tool) return tool
}
showTool(name) {
if (this._curTool === name) {
return this
}
this._curTool = name
const tools = this._tools
const tool = tools[name]
if (!tool) return
let lastTool = {}
each(tools, (tool) => {
if (tool.active) {
lastTool = tool
tool.active = false
tool.hide()
}
})
tool.active = true
tool.show()
this._tab.select(name)
this.emit('showTool', name, lastTool)
return this
}
initCfg(settings) {
const cfg = (this.config = Settings.createCfg('dev-tools', this._defCfg))
this._setTransparency(cfg.get('transparency'))
this._setDisplaySize(cfg.get('displaySize'))
this._setTheme(cfg.get('theme'))
cfg.on('change', (key, val) => {
switch (key) {
case 'transparency':
return this._setTransparency(val)
case 'displaySize':
return this._setDisplaySize(val)
case 'theme':
return this._setTheme(val)
}
})
settings
.separator()
.select(cfg, 'theme', 'Theme', [
'System preference',
...keys(evalCss.getThemes()),
])
if (!this._inline) {
settings
.range(cfg, 'transparency', 'Transparency', {
min: 0.2,
max: 1,
step: 0.01,
})
.range(cfg, 'displaySize', 'Display Size', {
min: 40,
max: 100,
step: 1,
})
}
settings
.button('Restore defaults and reload', function () {
const store = safeStorage('local')
const data = JSON.parse(JSON.stringify(store))
each(data, (val, key) => {
if (!isStr(val)) {
return
}
if (startWith(key, 'eruda')) {
store.removeItem(key)
}
})
window.location.reload()
})
.separator()
}
notify(content, options) {
this._notification.notify(content, options)
}
destroy() {
evalCss.remove(this._style)
this.removeAll()
this._tab.destroy()
this._$el.remove()
window.removeEventListener('resize', this._checkSafeArea)
emitter.off(emitter.SCALE, this._updateTabHeight)
}
_checkSafeArea = () => {
const { $container } = this
if (hasSafeArea()) {
$container.addClass(c('safe-area'))
} else {
$container.rmClass(c('safe-area'))
}
}
_setTheme(t) {
const { $container } = this
if (t === 'System preference') {
t = upperFirst(theme.get())
}
if (isDarkTheme(t)) {
$container.addClass(c('dark'))
} else {
$container.rmClass(c('dark'))
}
evalCss.setTheme(t)
}
_setTransparency(opacity) {
if (!isNum(opacity)) return
this._opacity = opacity
if (this._isShow) this._$el.css({ opacity })
}
_setDisplaySize(height) {
if (this._inline) {
height = 100
}
if (!isNum(height)) return
this._$el.css({ height: height + '%' })
}
_initTpl() {
const $container = this.$container
$container.append(
c(`
`)
)
this._$el = $container.find(c('.dev-tools'))
this._$tools = this._$el.find(c('.tools'))
}
_initTab() {
this._tab = new LunaTab(this._$el.find(c('.tab')).get(0), {
height: 40,
})
this._tab.on('select', (id) => this.showTool(id))
}
_updateTabHeight = (scale) => {
this._tab.setOption('height', 40 * scale)
nextTick(() => {
this._tab.updateSlider()
})
}
_initNotification() {
this._notification = new LunaNotification(
this._$el.find(c('.notification')).get(0),
{
position: {
x: 'center',
y: 'top',
},
}
)
}
_initModal() {
LunaModal.setContainer(this._$el.find(c('.modal')).get(0))
}
_bindEvent() {
const $resizer = this._$el.find(c('.resizer'))
const $navBar = this._$el.find(c('.nav-bar'))
const $document = $(document)
if (this._inline) {
$resizer.hide()
}
const startListener = (e) => {
e.preventDefault()
e.stopPropagation()
e = e.origEvent
this._isResizing = true
this._resizeStartSize = this.config.get('displaySize')
this._resizeStartY = eventClient('y', e)
$resizer.css('height', '100%')
$document.on(pointerEvent('move'), moveListener)
$document.on(pointerEvent('up'), endListener)
}
const moveListener = (e) => {
if (!this._isResizing) {
return
}
e.preventDefault()
e.stopPropagation()
e = e.origEvent
const deltaY =
((this._resizeStartY - eventClient('y', e)) / window.innerHeight) * 100
let displaySize = this._resizeStartSize + deltaY
if (displaySize < 40) {
displaySize = 40
} else if (displaySize > 100) {
displaySize = 100
}
this.config.set('displaySize', toNum(displaySize.toFixed(2)))
}
const endListener = () => {
clearTimeout(this._resizeTimer)
this._isResizing = false
$resizer.css('height', 10)
$document.off(pointerEvent('move'), moveListener)
$document.off(pointerEvent('up'), endListener)
}
$resizer.css('height', 10)
$resizer.on(pointerEvent('down'), startListener)
$navBar.on('contextmenu', (e) => e.preventDefault())
this.$container.on('click', (e) => e.stopPropagation())
window.addEventListener('resize', this._checkSafeArea)
emitter.on(emitter.SCALE, this._updateTabHeight)
theme.on('change', () => {
const t = this.config.get('theme')
if (t === 'System preference') {
this._setTheme(t)
}
})
}
}
================================================
FILE: src/DevTools/DevTools.scss
================================================
@use '../style/variable' as *;
@use '../style/mixin' as *;
.dev-tools {
position: absolute;
width: 100%;
height: 100%;
left: 0;
bottom: 0;
background: var(--background);
z-index: 500;
display: none;
padding-top: 40px !important;
opacity: 0;
transition: opacity $anim-duration;
border-top: 1px solid var(--border);
.resizer {
position: absolute;
width: 100%;
touch-action: none;
left: 0;
top: -8px;
cursor: row-resize;
z-index: 120;
}
.tools {
@include overflow-auto();
height: 100%;
width: 100%;
position: relative;
.tool {
@include absolute();
overflow: hidden;
display: none;
}
}
}
================================================
FILE: src/DevTools/Tool.js
================================================
import Class from 'licia/Class'
export default Class({
init($el) {
this._$el = $el
},
show() {
this._$el.show()
return this
},
hide() {
this._$el.hide()
return this
},
destroy() {
this._$el.remove()
},
})
================================================
FILE: src/Elements/CssStore.js
================================================
import each from 'licia/each'
import sortKeys from 'licia/sortKeys'
function formatStyle(style) {
const ret = {}
for (let i = 0, len = style.length; i < len; i++) {
const name = style[i]
if (style[name] === 'initial') continue
ret[name] = style[name]
}
return sortStyleKeys(ret)
}
const elProto = Element.prototype
let matchesSel = function () {
return false
}
if (elProto.webkitMatchesSelector) {
matchesSel = (el, selText) => el.webkitMatchesSelector(selText)
} else if (elProto.mozMatchesSelector) {
matchesSel = (el, selText) => el.mozMatchesSelector(selText)
}
export default class CssStore {
constructor(el) {
this._el = el
}
getComputedStyle() {
const computedStyle = window.getComputedStyle(this._el)
return formatStyle(computedStyle)
}
getMatchedCSSRules() {
const ret = []
each(document.styleSheets, (styleSheet) => {
try {
// Started with version 64, Chrome does not allow cross origin script to access this property.
if (!styleSheet.cssRules) return
} catch {
return
}
each(styleSheet.cssRules, (cssRule) => {
let matchesEl = false
// Mobile safari will throw DOM Exception 12 error, need to try catch it.
try {
matchesEl = this._elMatchesSel(cssRule.selectorText)
} catch {
// No op
}
if (!matchesEl) return
ret.push({
selectorText: cssRule.selectorText,
style: formatStyle(cssRule.style),
})
})
})
return ret
}
_elMatchesSel(selText) {
return matchesSel(this._el, selText)
}
}
function sortStyleKeys(style) {
return sortKeys(style, {
comparator: (a, b) => {
const lenA = a.length
const lenB = b.length
const len = lenA > lenB ? lenB : lenA
for (let i = 0; i < len; i++) {
const codeA = a.charCodeAt(i)
const codeB = b.charCodeAt(i)
const cmpResult = cmpCode(codeA, codeB)
if (cmpResult !== 0) return cmpResult
}
if (lenA > lenB) return 1
if (lenA < lenB) return -1
return 0
},
})
}
function cmpCode(a, b) {
a = transCode(a)
b = transCode(b)
if (a > b) return 1
if (a < b) return -1
return 0
}
function transCode(code) {
// - should be placed after lowercase chars.
if (code === 45) return 123
return code
}
================================================
FILE: src/Elements/Detail.js
================================================
import isEmpty from 'licia/isEmpty'
import lowerCase from 'licia/lowerCase'
import pick from 'licia/pick'
import toStr from 'licia/toStr'
import map from 'licia/map'
import isEl from 'licia/isEl'
import escape from 'licia/escape'
import startWith from 'licia/startWith'
import contain from 'licia/contain'
import unique from 'licia/unique'
import each from 'licia/each'
import keys from 'licia/keys'
import isNull from 'licia/isNull'
import trim from 'licia/trim'
import isFn from 'licia/isFn'
import isBool from 'licia/isBool'
import safeGet from 'licia/safeGet'
import $ from 'licia/$'
import h from 'licia/h'
import extend from 'licia/extend'
import MutationObserver from 'licia/MutationObserver'
import CssStore from './CssStore'
import Settings from '../Settings/Settings'
import LunaModal from 'luna-modal'
import LunaBoxModel from 'luna-box-model'
import chobitsu from '../lib/chobitsu'
import { formatNodeName } from './util'
import { isErudaEl, classPrefix as c } from '../lib/util'
export default class Detail {
constructor($container, devtools) {
this._$container = $container
this._devtools = devtools
this._curEl = document.documentElement
this._initObserver()
this._initCfg()
this._initTpl()
this._bindEvent()
}
show(el) {
this._curEl = el
this._rmDefComputedStyle = true
this._computedStyleSearchKeyword = ''
this._enableObserver()
this._render()
this._highlight()
}
hide = () => {
this._$container.hide()
this._disableObserver()
chobitsu.domain('Overlay').hideHighlight()
}
destroy() {
this._disableObserver()
this.restoreEventTarget()
this._rmCfg()
}
overrideEventTarget() {
const winEventProto = getWinEventProto()
const origAddEvent = (this._origAddEvent = winEventProto.addEventListener)
const origRmEvent = (this._origRmEvent = winEventProto.removeEventListener)
winEventProto.addEventListener = function (type, listener, useCapture) {
addEvent(this, type, listener, useCapture)
origAddEvent.apply(this, arguments)
}
winEventProto.removeEventListener = function (type, listener, useCapture) {
rmEvent(this, type, listener, useCapture)
origRmEvent.apply(this, arguments)
}
}
restoreEventTarget() {
const winEventProto = getWinEventProto()
if (this._origAddEvent) winEventProto.addEventListener = this._origAddEvent
if (this._origRmEvent) winEventProto.removeEventListener = this._origRmEvent
}
_highlight = (type) => {
const el = this._curEl
const highlightConfig = {
showInfo: false,
}
if (!type || type === 'all') {
extend(highlightConfig, {
showInfo: true,
contentColor: 'rgba(111, 168, 220, .66)',
paddingColor: 'rgba(147, 196, 125, .55)',
borderColor: 'rgba(255, 229, 153, .66)',
marginColor: 'rgba(246, 178, 107, .66)',
})
} else if (type === 'margin') {
highlightConfig.marginColor = 'rgba(246, 178, 107, .66)'
} else if (type === 'border') {
highlightConfig.borderColor = 'rgba(255, 229, 153, .66)'
} else if (type === 'padding') {
highlightConfig.paddingColor = 'rgba(147, 196, 125, .55)'
} else if (type === 'content') {
highlightConfig.contentColor = 'rgba(111, 168, 220, .66)'
}
const { nodeId } = chobitsu.domain('DOM').getNodeId({ node: el })
chobitsu.domain('Overlay').highlightNode({
nodeId,
highlightConfig,
})
}
_initTpl() {
const $container = this._$container
const html = `
`
$container.html(html)
this._$elementName = $container.find(c('.element-name'))
this._$attributes = $container.find(c('.attributes'))
this._$styles = $container.find(c('.styles'))
this._$listeners = $container.find(c('.listeners'))
this._$computedStyle = $container.find(c('.computed-style'))
const boxModelContainer = h('div')
this._$boxModel = $(boxModelContainer)
this._boxModel = new LunaBoxModel(boxModelContainer)
}
_toggleAllComputedStyle() {
this._rmDefComputedStyle = !this._rmDefComputedStyle
this._render()
}
_render() {
const data = this._getData(this._curEl)
const $attributes = this._$attributes
const $elementName = this._$elementName
const $styles = this._$styles
const $computedStyle = this._$computedStyle
const $listeners = this._$listeners
$elementName.html(data.name)
let attributes = 'Empty '
if (!isEmpty(data.attributes)) {
attributes = map(data.attributes, ({ name, value }) => {
return `
${escape(name)}
${value}
`
}).join('')
}
attributes = `Attributes
`
$attributes.html(attributes)
let styles = ''
if (!isEmpty(data.styles)) {
const style = map(data.styles, ({ selectorText, style }) => {
style = map(style, (val, key) => {
return `${escape(
key
)} : ${val};
`
}).join('')
return `
${escape(selectorText)} {
${style}
}
`
}).join('')
styles = `Styles
${style}
`
$styles.html(styles).show()
} else {
$styles.hide()
}
let computedStyle = ''
if (data.computedStyle) {
let toggleButton = c(`
`)
if (data.rmDefComputedStyle) {
toggleButton = c(`
`)
}
computedStyle = `
Computed Style
${toggleButton}
${
data.computedStyleSearchKeyword
? `${escape(
data.computedStyleSearchKeyword
)}
`
: ''
}
${map(data.computedStyle, (val, key) => {
return `
${escape(key)}
${val}
`
}).join('')}
`
$computedStyle.html(computedStyle).show()
this._boxModel.setOption('element', this._curEl)
$computedStyle.find(c('.box-model')).append(this._$boxModel.get(0))
} else {
$computedStyle.text('').hide()
}
let listeners = ''
if (data.listeners) {
listeners = map(data.listeners, (listeners, key) => {
listeners = map(listeners, ({ useCapture, listenerStr }) => {
return `${escape(
listenerStr
)} `
}).join('')
return ``
}).join('')
listeners = `Event Listeners
${listeners}
`
$listeners.html(listeners).show()
} else {
$listeners.hide()
}
this._$container.show()
}
_getData(el) {
const ret = {}
const cssStore = new CssStore(el)
const { className, id, attributes, tagName } = el
ret.computedStyleSearchKeyword = this._computedStyleSearchKeyword
ret.attributes = formatAttr(attributes)
ret.name = formatNodeName({ tagName, id, className, attributes })
const events = el.erudaEvents
if (events && keys(events).length !== 0) ret.listeners = events
if (needNoStyle(tagName)) {
return ret
}
let computedStyle = cssStore.getComputedStyle()
const styles = cssStore.getMatchedCSSRules()
styles.unshift(getInlineStyle(el.style))
styles.forEach((style) => processStyleRules(style.style))
ret.styles = styles
if (this._rmDefComputedStyle) {
computedStyle = rmDefComputedStyle(computedStyle, styles)
}
ret.rmDefComputedStyle = this._rmDefComputedStyle
const computedStyleSearchKeyword = lowerCase(ret.computedStyleSearchKeyword)
if (computedStyleSearchKeyword) {
computedStyle = pick(computedStyle, (val, property) => {
return (
contain(property, computedStyleSearchKeyword) ||
contain(val, computedStyleSearchKeyword)
)
})
}
processStyleRules(computedStyle)
ret.computedStyle = computedStyle
return ret
}
_bindEvent() {
const devtools = this._devtools
this._$container
.on('click', c('.toggle-all-computed-style'), () =>
this._toggleAllComputedStyle()
)
.on('click', c('.computed-style-search'), () => {
LunaModal.prompt('Filter').then((filter) => {
if (isNull(filter)) return
filter = trim(filter)
this._computedStyleSearchKeyword = filter
this._render()
})
})
.on('click', '.eruda-listener-content', function () {
const text = $(this).text()
const sources = devtools.get('sources')
if (sources) {
sources.set('js', text)
devtools.showTool('sources')
}
})
.on('click', c('.element-name'), () => {
const sources = devtools.get('sources')
if (sources) {
sources.set('object', this._curEl)
devtools.showTool('sources')
}
})
.on('click', c('.back'), this.hide)
.on('click', c('.refresh'), () => {
this._render()
devtools.notify('Refreshed', { icon: 'success' })
})
this._boxModel.on('highlight', this._highlight)
}
_initObserver() {
this._observer = new MutationObserver((mutations) => {
each(mutations, (mutation) => this._handleMutation(mutation))
})
}
_enableObserver() {
this._observer.observe(document.documentElement, {
attributes: true,
childList: true,
subtree: true,
})
}
_disableObserver() {
this._observer.disconnect()
}
_handleMutation(mutation) {
if (isErudaEl(mutation.target)) return
if (mutation.type === 'attributes') {
if (mutation.target !== this._curEl) return
this._render()
}
}
_rmCfg() {
const cfg = this.config
const settings = this._devtools.get('settings')
if (!settings) return
settings
.remove(cfg, 'overrideEventTarget')
.remove(cfg, 'observeElement')
.remove('Elements')
}
_initCfg() {
const cfg = (this.config = Settings.createCfg('elements', {
overrideEventTarget: true,
}))
if (cfg.get('overrideEventTarget')) this.overrideEventTarget()
cfg.on('change', (key, val) => {
switch (key) {
case 'overrideEventTarget':
return val ? this.overrideEventTarget() : this.restoreEventTarget()
}
})
const settings = this._devtools.get('settings')
if (!settings) return
settings
.text('Elements')
.switch(cfg, 'overrideEventTarget', 'Catch Event Listeners')
settings.separator()
}
}
function processStyleRules(style) {
each(style, (val, key) => (style[key] = processStyleRule(val)))
}
const formatAttr = (attributes) =>
map(attributes, (attr) => {
let { value } = attr
const { name } = attr
value = escape(value)
const isLink =
(name === 'src' || name === 'href') && !startWith(value, 'data')
if (isLink) value = wrapLink(value)
if (name === 'style') value = processStyleRule(value)
return { name, value }
})
const regColor = /rgba?\((.*?)\)/g
const regCssUrl = /url\("?(.*?)"?\)/g
function processStyleRule(val) {
// For css custom properties, val is unable to retrieved.
val = toStr(val)
return val
.replace(
regColor,
' $&'
)
.replace(regCssUrl, (match, url) => `url("${wrapLink(url)}")`)
}
function getInlineStyle(style) {
const ret = {
selectorText: 'element.style',
style: {},
}
for (let i = 0, len = style.length; i < len; i++) {
const s = style[i]
ret.style[s] = style[s]
}
return ret
}
function rmDefComputedStyle(computedStyle, styles) {
const ret = {}
let keepStyles = ['display', 'width', 'height']
each(styles, (style) => {
keepStyles = keepStyles.concat(keys(style.style))
})
keepStyles = unique(keepStyles)
each(computedStyle, (val, key) => {
if (!contain(keepStyles, key)) return
ret[key] = val
})
return ret
}
const NO_STYLE_TAG = ['script', 'style', 'meta', 'title', 'link', 'head']
const needNoStyle = (tagName) => {
NO_STYLE_TAG.indexOf(tagName.toLowerCase()) > -1
}
const wrapLink = (link) => `${link} `
function addEvent(el, type, listener, useCapture = false) {
if (!isEl(el) || !isFn(listener) || !isBool(useCapture)) return
const events = (el.erudaEvents = el.erudaEvents || {})
events[type] = events[type] || []
events[type].push({
listener: listener,
listenerStr: listener.toString(),
useCapture: useCapture,
})
}
function rmEvent(el, type, listener, useCapture = false) {
if (!isEl(el) || !isFn(listener) || !isBool(useCapture)) return
const events = el.erudaEvents
if (!(events && events[type])) return
const listeners = events[type]
for (let i = 0, len = listeners.length; i < len; i++) {
if (listeners[i].listener === listener) {
listeners.splice(i, 1)
break
}
}
if (listeners.length === 0) delete events[type]
if (keys(events).length === 0) delete el.erudaEvents
}
const getWinEventProto = () => {
return safeGet(window, 'EventTarget.prototype') || window.Node.prototype
}
================================================
FILE: src/Elements/Elements.js
================================================
import Tool from '../DevTools/Tool'
import $ from 'licia/$'
import isEl from 'licia/isEl'
import nextTick from 'licia/nextTick'
import Emitter from 'licia/Emitter'
import map from 'licia/map'
import MediaQuery from 'licia/MediaQuery'
import isEmpty from 'licia/isEmpty'
import toNum from 'licia/toNum'
import copy from 'licia/copy'
import isMobile from 'licia/isMobile'
import isShadowRoot from 'licia/isShadowRoot'
import LunaDomViewer from 'luna-dom-viewer'
import { isErudaEl, classPrefix as c, isChobitsuEl } from '../lib/util'
import evalCss from '../lib/evalCss'
import Detail from './Detail'
import chobitsu from '../lib/chobitsu'
import emitter from '../lib/emitter'
import { formatNodeName } from './util'
export default class Elements extends Tool {
constructor() {
super()
this._style = evalCss(require('./Elements.scss'))
this.name = 'elements'
this._selectElement = false
this._observeElement = true
this._history = []
Emitter.mixin(this)
}
init($el, container) {
super.init($el)
this._container = container
this._initTpl()
this._htmlEl = document.documentElement
this._detail = new Detail(this._$detail, container)
this.config = this._detail.config
this._splitMediaQuery = new MediaQuery('screen and (min-width: 680px)')
this._splitMode = this._splitMediaQuery.isMatch()
this._domViewer = new LunaDomViewer(this._$domViewer.get(0), {
node: this._htmlEl,
ignore: (node) => isErudaEl(node) || isChobitsuEl(node),
})
this._domViewer.expand()
this._bindEvent()
chobitsu.domain('Overlay').enable()
nextTick(() => this._updateHistory())
}
show() {
super.show()
this._isShow = true
if (!this._curNode) {
this.select(document.body)
} else if (this._splitMode) {
this._showDetail()
}
}
hide() {
super.hide()
this._isShow = false
chobitsu.domain('Overlay').hideHighlight()
}
select(node) {
this._domViewer.select(node)
this._setNode(node)
this.emit('change', node)
return this
}
destroy() {
super.destroy()
emitter.off(emitter.SCALE, this._updateScale)
evalCss.remove(this._style)
this._detail.destroy()
chobitsu
.domain('Overlay')
.off('inspectNodeRequested', this._inspectNodeRequested)
chobitsu.domain('Overlay').disable()
this._splitMediaQuery.removeAllListeners()
}
_updateButtons() {
const $control = this._$control
const $showDetail = $control.find(c('.show-detail'))
const $copyNode = $control.find(c('.copy-node'))
const $deleteNode = $control.find(c('.delete-node'))
const iconDisabled = c('icon-disabled')
$showDetail.addClass(iconDisabled)
$copyNode.addClass(iconDisabled)
$deleteNode.addClass(iconDisabled)
const node = this._curNode
if (!node || isShadowRoot(node)) {
return
}
if (node !== document.documentElement && node !== document.body) {
$deleteNode.rmClass(iconDisabled)
}
$copyNode.rmClass(iconDisabled)
if (node.nodeType === Node.ELEMENT_NODE) {
$showDetail.rmClass(iconDisabled)
}
}
_showDetail = () => {
if (!this._isShow || !this._curNode) {
return
}
if (this._curNode.nodeType === Node.ELEMENT_NODE) {
this._detail.show(this._curNode)
} else {
this._detail.show(this._curNode.parentNode || this._curNode.host)
}
}
_initTpl() {
const $el = this._$el
$el.html(
c(`
`)
)
this._$detail = $el.find(c('.detail'))
this._$domViewer = $el.find(c('.dom-viewer'))
this._$control = $el.find(c('.control'))
this._$crumbs = $el.find(c('.crumbs'))
}
_renderCrumbs() {
const crumbs = getCrumbs(this._curNode)
let html = ''
if (!isEmpty(crumbs)) {
html = map(crumbs, ({ text, idx }) => {
return `${text} `
}).join('')
}
this._$crumbs.html(html)
}
_back = () => {
if (this._curNode === this._htmlEl) return
const parentQueue = this._curParentQueue
let parent = parentQueue.shift()
while (!isElExist(parent)) {
parent = parentQueue.shift()
}
this.set(parent)
}
_bindEvent() {
const self = this
this._$el.on('click', c('.crumb'), function () {
let idx = toNum($(this).data('idx'))
let node = self._curNode
while (idx-- && node.parentElement) {
node = node.parentElement
}
if (isElExist(node)) {
self.select(node)
}
})
this._$control
.on('click', c('.select'), this._toggleSelect)
.on('click', c('.show-detail'), this._showDetail)
.on('click', c('.copy-node'), this._copyNode)
.on('click', c('.delete-node'), this._deleteNode)
this._domViewer.on('select', this._setNode).on('deselect', this._back)
chobitsu
.domain('Overlay')
.on('inspectNodeRequested', this._inspectNodeRequested)
this._splitMediaQuery.on('match', () => {
this._splitMode = true
this._showDetail()
})
this._splitMediaQuery.on('unmatch', () => {
this._splitMode = false
this._detail.hide()
})
emitter.on(emitter.SCALE, this._updateScale)
}
_updateScale = (scale) => {
this._splitMediaQuery.setQuery(`screen and (min-width: ${680 * scale}px)`)
}
_deleteNode = () => {
const node = this._curNode
if (node.parentNode) {
node.parentNode.removeChild(node)
}
}
_copyNode = () => {
const node = this._curNode
if (node.nodeType === Node.ELEMENT_NODE) {
copy(node.outerHTML)
} else {
copy(node.nodeValue)
}
this._container.notify('Copied', { icon: 'success' })
}
_toggleSelect = () => {
this._$el.find(c('.select')).toggleClass(c('active'))
this._selectElement = !this._selectElement
if (this._selectElement) {
chobitsu.domain('Overlay').setInspectMode({
mode: 'searchForNode',
highlightConfig: {
showInfo: !isMobile(),
showRulers: false,
showAccessibilityInfo: !isMobile(),
showExtensionLines: false,
contrastAlgorithm: 'aa',
contentColor: 'rgba(111, 168, 220, .66)',
paddingColor: 'rgba(147, 196, 125, .55)',
borderColor: 'rgba(255, 229, 153, .66)',
marginColor: 'rgba(246, 178, 107, .66)',
},
})
this._container.hide()
} else {
chobitsu.domain('Overlay').setInspectMode({
mode: 'none',
})
chobitsu.domain('Overlay').hideHighlight()
}
}
_inspectNodeRequested = ({ backendNodeId }) => {
this._container.show()
this._toggleSelect()
try {
const { node } = chobitsu.domain('DOM').getNode({ nodeId: backendNodeId })
this.select(node)
} catch {
// No op
}
}
_setNode = (node) => {
if (node === this._curNode) return
this._curNode = node
this._renderCrumbs()
const parentQueue = []
let parent = node.parentNode
while (parent) {
parentQueue.push(parent)
parent = parent.parentNode
}
this._curParentQueue = parentQueue
if (this._splitMode) {
this._showDetail()
}
this._updateButtons()
this._updateHistory()
}
_updateHistory() {
const console = this._container.get('console')
if (!console) return
const history = this._history
history.unshift(this._curNode)
if (history.length > 5) history.pop()
for (let i = 0; i < 5; i++) {
console.setGlobal(`$${i}`, history[i])
}
}
}
const isElExist = (val) => isEl(val) && val.parentNode
function getCrumbs(el) {
const ret = []
let i = 0
while (el) {
ret.push({
text: formatNodeName(el, { noAttr: true }),
idx: i++,
})
if (isShadowRoot(el)) {
el = el.host
}
if (!el.parentElement && isShadowRoot(el.parentNode)) {
el = el.parentNode
} else {
el = el.parentElement
}
}
return ret.reverse()
}
================================================
FILE: src/Elements/Elements.scss
================================================
@use '../style/variable' as *;
@use '../style/mixin' as *;
#elements {
.elements {
@include absolute();
padding-top: 40px;
padding-bottom: 24px;
font-size: 14px;
}
.control {
padding: 10px 0;
@include control();
.icon-eye {
right: 0;
}
.icon-copy {
right: 23px;
}
.icon-delete {
right: 46px;
}
}
.dom-viewer-container {
@include overflow-auto();
height: 100%;
padding: 5px 0;
}
.crumbs {
@include absolute(100%, 24px);
top: initial;
line-height: 24px;
bottom: 0;
border-top: 1px solid var(--border);
background: var(--darker-background);
color: var(--primary);
font-size: $font-size-s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
li {
cursor: pointer;
padding: 0 7px;
display: inline-block;
&:hover,
&:last-child {
background: var(--highlight);
}
}
}
.detail {
@include absolute();
z-index: 10;
padding-top: 40px;
display: none;
background: var(--background);
.control {
padding: 10px 35px;
.element-name {
font-size: $font-size-s;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
display: inline-block;
}
.icon-left {
left: 0;
}
.icon-refresh {
right: 0;
}
}
.element {
@include overflow-auto(y);
height: 100%;
}
}
.section {
border-bottom: 1px solid var(--border);
color: var(--foreground);
margin: 10px 0;
h2 {
color: var(--primary);
background: var(--darker-background);
border-top: 1px solid var(--border);
padding: $padding;
line-height: 18px;
font-size: $font-size;
transition: background-color $anim-duration;
@include right-btn();
&.active-effect {
cursor: pointer;
}
&.active-effect:active {
background: var(--highlight);
color: var(--select-foreground);
}
}
}
.attributes {
font-size: $font-size-s;
a {
color: var(--link-color);
}
.table-wrapper {
@include overflow-auto(x);
}
table {
td {
padding: 5px 10px;
}
}
}
.text-content {
background: #fff;
.content {
@include overflow-auto(x);
padding: $padding;
}
}
.style-color {
position: relative;
top: 1px;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 2px;
border: 1px solid var(--border);
display: inline-block;
}
.box-model {
@include overflow-auto(x);
padding: $padding;
text-align: center;
border-bottom: 1px solid var(--color);
}
.computed-style {
font-size: $font-size-s;
a {
color: var(--link-color);
}
.table-wrapper {
@include overflow-auto(y);
max-height: 200px;
border-top: 1px solid var(--border);
}
table {
td {
padding: 5px 10px;
&.key {
white-space: nowrap;
color: var(--var-color);
}
}
}
}
.styles {
font-size: $font-size-s;
.style-wrapper {
padding: $padding;
.style-rules {
border: 1px solid var(--border);
padding: $padding;
margin-bottom: 10px;
.rule {
padding-left: 2em;
word-break: break-all;
a {
color: var(--link-color);
}
span {
color: var(--var-color);
}
}
&:last-child {
margin-bottom: 0;
}
}
}
}
.listeners {
font-size: $font-size-s;
.listener-wrapper {
padding: $padding;
.listener {
margin-bottom: 10px;
overflow: hidden;
border: 1px solid var(--border);
.listener-type {
padding: $padding;
background: var(--darker-background);
color: var(--primary);
}
.listener-content {
li {
@include overflow-auto(x);
padding: $padding;
border-top: none;
}
}
}
}
}
}
.safe-area #elements {
.elements {
@include safe-area(padding-bottom, 24px);
}
.crumbs {
@include safe-area(height, 24px);
}
.element {
@include safe-area(padding-bottom, 0px);
}
}
@media screen and (min-width: 680px) {
#elements {
.elements {
width: 50%;
.control {
.icon-eye {
display: none;
}
.icon-copy {
right: 0;
}
.icon-delete {
right: 23px;
}
}
}
.detail {
width: 50%;
left: initial;
right: 0;
border-left: 1px solid var(--border);
.control {
padding-left: 10px;
.icon-left {
display: none;
}
}
}
}
}
================================================
FILE: src/Elements/util.js
================================================
import each from 'licia/each'
import isStr from 'licia/isStr'
import isShadowRoot from 'licia/isShadowRoot'
import { classPrefix as c } from '../lib/util'
export function formatNodeName(node, { noAttr = false } = {}) {
if (node.nodeType === Node.TEXT_NODE) {
return `(text) `
} else if (node.nodeType === Node.COMMENT_NODE) {
return ` `
} else if (isShadowRoot(node)) {
return `#shadow-root `
}
const { id, className, attributes } = node
let ret = `${node.tagName.toLowerCase()} `
if (id !== '') ret += `#${id} `
if (isStr(className)) {
let classes = ''
each(className.split(/\s+/g), (val) => {
if (val.trim() === '') return
classes += `.${val}`
})
ret += `${classes} `
}
if (!noAttr) {
each(attributes, (attr) => {
const name = attr.name
if (name === 'id' || name === 'class' || name === 'style') return
ret += ` ${name} =" ${attr.value} " `
})
}
return ret
}
================================================
FILE: src/EntryBtn/EntryBtn.js
================================================
import emitter from '../lib/emitter'
import Settings from '../Settings/Settings'
import Emitter from 'licia/Emitter'
import $ from 'licia/$'
import nextTick from 'licia/nextTick'
import orientation from 'licia/orientation'
import pointerEvent from 'licia/pointerEvent'
import { pxToNum, classPrefix as c, eventClient } from '../lib/util'
import evalCss from '../lib/evalCss'
const $document = $(document)
export default class EntryBtn extends Emitter {
constructor($container) {
super()
this._style = evalCss(require('./EntryBtn.scss'))
this._$container = $container
this._initTpl()
this._bindEvent()
this._registerListener()
}
hide() {
this._$el.hide()
}
show() {
this._$el.show()
}
setPos(pos) {
if (this._isOutOfRange(pos)) {
pos = this._getDefPos()
}
this._$el.css({
left: pos.x,
top: pos.y,
})
this.config.set('pos', pos)
}
getPos() {
return this.config.get('pos')
}
destroy() {
evalCss.remove(this._style)
this._unregisterListener()
this._$el.remove()
}
_isOutOfRange(pos) {
pos = pos || this.config.get('pos')
const defPos = this._getDefPos()
return (
pos.x > defPos.x + 10 || pos.x < 0 || pos.y < 0 || pos.y > defPos.y + 10
)
}
_registerListener() {
this._scaleListener = () =>
nextTick(() => {
if (this._isOutOfRange()) this._resetPos()
})
emitter.on(emitter.SCALE, this._scaleListener)
}
_unregisterListener() {
emitter.off(emitter.SCALE, this._scaleListener)
}
_initTpl() {
const $container = this._$container
$container.append(
c('
')
)
this._$el = $container.find('.eruda-entry-btn')
}
_resetPos(orientationChanged) {
const cfg = this.config
let pos = cfg.get('pos')
const defPos = this._getDefPos()
if (!cfg.get('rememberPos') || orientationChanged) {
pos = defPos
}
this.setPos(pos)
}
_onDragStart = (e) => {
const $el = this._$el
$el.addClass(c('active'))
this._isClick = true
e = e.origEvent
this._startX = eventClient('x', e)
this._oldX = pxToNum($el.css('left'))
this._oldY = pxToNum($el.css('top'))
this._startY = eventClient('y', e)
$document.on(pointerEvent('move'), this._onDragMove)
$document.on(pointerEvent('up'), this._onDragEnd)
}
_onDragMove = (e) => {
const btnSize = this._$el.get(0).offsetWidth
const maxWidth = this._$container.get(0).offsetWidth
const maxHeight = this._$container.get(0).offsetHeight
e = e.origEvent
const deltaX = eventClient('x', e) - this._startX
const deltaY = eventClient('y', e) - this._startY
if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) {
this._isClick = false
}
let newX = this._oldX + deltaX
let newY = this._oldY + deltaY
if (newX < 0) {
newX = 0
} else if (newX > maxWidth - btnSize) {
newX = maxWidth - btnSize
}
if (newY < 0) {
newY = 0
} else if (newY > maxHeight - btnSize) {
newY = maxHeight - btnSize
}
this._$el.css({
left: newX,
top: newY,
})
}
_onDragEnd = (e) => {
const $el = this._$el
if (this._isClick) {
this.emit('click')
}
this._onDragMove(e)
$document.off(pointerEvent('move'), this._onDragMove)
$document.off(pointerEvent('up'), this._onDragEnd)
const cfg = this.config
if (cfg.get('rememberPos')) {
cfg.set('pos', {
x: pxToNum($el.css('left')),
y: pxToNum($el.css('top')),
})
}
$el.rmClass('eruda-active')
}
_bindEvent() {
const $el = this._$el
$el.on(pointerEvent('down'), this._onDragStart)
orientation.on('change', () => this._resetPos(true))
window.addEventListener('resize', () => this._resetPos())
}
initCfg(settings) {
const cfg = (this.config = Settings.createCfg('entry-button', {
rememberPos: true,
pos: this._getDefPos(),
}))
settings.switch(cfg, 'rememberPos', 'Remember Entry Button Position')
this._resetPos()
}
_getDefPos() {
const minWidth = this._$el.get(0).offsetWidth + 10
return {
x: window.innerWidth - minWidth,
y: window.innerHeight - minWidth,
}
}
}
================================================
FILE: src/EntryBtn/EntryBtn.scss
================================================
.container {
.entry-btn {
touch-action: none;
width: 40px;
height: 40px;
display: flex;
background: #000;
opacity: 0.3;
border-radius: 10px;
position: relative;
z-index: 1000;
transition: opacity 0.3s;
color: #fff;
font-size: 25px;
align-items: center;
justify-content: center;
&.active,
&:active {
opacity: 0.8;
}
}
}
================================================
FILE: src/Info/Info.js
================================================
import Tool from '../DevTools/Tool'
import defInfo from './defInfo'
import each from 'licia/each'
import isFn from 'licia/isFn'
import isUndef from 'licia/isUndef'
import cloneDeep from 'licia/cloneDeep'
import evalCss from '../lib/evalCss'
import map from 'licia/map'
import escape from 'licia/escape'
import copy from 'licia/copy'
import $ from 'licia/$'
import { classPrefix as c } from '../lib/util'
export default class Info extends Tool {
constructor() {
super()
this._style = evalCss(require('./Info.scss'))
this.name = 'info'
this._infos = []
}
init($el, container) {
super.init($el)
this._container = container
this._addDefInfo()
this._bindEvent()
}
destroy() {
super.destroy()
evalCss.remove(this._style)
}
add(name, val) {
const infos = this._infos
let isUpdate = false
each(infos, (info) => {
if (name !== info.name) return
info.val = val
isUpdate = true
})
if (!isUpdate) infos.push({ name, val })
this._render()
return this
}
get(name) {
const infos = this._infos
if (isUndef(name)) {
return cloneDeep(infos)
}
let result
each(infos, (info) => {
if (name === info.name) result = info.val
})
return result
}
remove(name) {
const infos = this._infos
for (let i = infos.length - 1; i >= 0; i--) {
if (infos[i].name === name) infos.splice(i, 1)
}
this._render()
return this
}
clear() {
this._infos = []
this._render()
return this
}
_addDefInfo() {
each(defInfo, (info) => this.add(info.name, info.val))
}
_render() {
const infos = []
each(this._infos, ({ name, val }) => {
if (isFn(val)) val = val()
infos.push({ name, val })
})
const html = `${map(
infos,
(info) =>
`${escape(info.name)} ${info.val}
`
).join('')} `
this._renderHtml(html)
}
_bindEvent() {
const container = this._container
this._$el.on('click', c('.copy'), function () {
const $li = $(this).parent().parent()
const name = $li.find(c('.title')).text()
const content = $li.find(c('.content')).text()
copy(`${name}: ${content}`)
container.notify('Copied', { icon: 'success' })
})
}
_renderHtml(html) {
if (html === this._lastHtml) return
this._lastHtml = html
this._$el.html(html)
}
}
================================================
FILE: src/Info/Info.scss
================================================
@use '../style/variable' as *;
@use '../style/mixin' as *;
#info {
@include overflow-auto(y);
li {
margin: 10px;
border: 1px solid var(--border);
.title,
.content {
padding: $padding;
}
.title {
position: relative;
padding-bottom: 0;
color: var(--accent);
.icon-copy {
position: absolute;
right: 10px;
top: 14px;
color: var(--primary);
cursor: pointer;
transition: color $anim-duration;
&:active {
color: var(--accent);
}
}
}
.content {
margin: 0;
user-select: text;
color: var(--foreground);
font-size: $font-size-s;
word-break: break-all;
table {
width: 100%;
border-collapse: collapse;
th,
td {
border: 1px solid var(--border);
padding: 10px;
}
}
* {
user-select: text;
}
a {
color: var(--link-color);
}
}
.device-key,
.system-key {
width: 100px;
}
}
}
.safe-area #info {
@include safe-area(padding-bottom, 10px);
}
================================================
FILE: src/Info/defInfo.js
================================================
import detectBrowser from 'licia/detectBrowser'
import detectOs from 'licia/detectOs'
import escape from 'licia/escape'
import map from 'licia/map'
const browser = detectBrowser()
export default [
{
name: 'Location',
val() {
return escape(location.href)
},
},
{
name: 'User Agent',
val: navigator.userAgent,
},
{
name: 'Device',
val: [
'',
`screen ${screen.width} * ${screen.height} `,
`viewport ${window.innerWidth} * ${window.innerHeight} `,
`pixel ratio ${window.devicePixelRatio} `,
'
',
].join(''),
},
{
name: 'System',
val: [
'',
`os ${detectOs()} `,
`browser ${
browser.name + ' ' + browser.version
} `,
'
',
].join(''),
},
{
name: 'Sponsor this Project',
val() {
return (
'' +
map(
[
{
name: 'Open Collective',
link: 'https://opencollective.com/eruda',
},
{
name: 'Ko-fi',
link: 'https://ko-fi.com/surunzi',
},
{
name: 'Wechat Pay',
link: 'https://surunzi.com/wechatpay.html',
},
],
(item) => {
return `${
item.name
} ${item.link.replace(
'https://',
''
)} `
}
).join(' ') +
'
'
)
},
},
{
name: 'About',
val:
'Eruda v' +
VERSION +
' ',
},
]
================================================
FILE: src/Network/Detail.js
================================================
import trim from 'licia/trim'
import isEmpty from 'licia/isEmpty'
import map from 'licia/map'
import each from 'licia/each'
import escape from 'licia/escape'
import copy from 'licia/copy'
import isJson from 'licia/isJson'
import Emitter from 'licia/Emitter'
import truncate from 'licia/truncate'
import { classPrefix as c } from '../lib/util'
export default class Detail extends Emitter {
constructor($container, devtools) {
super()
this._$container = $container
this._devtools = devtools
this._detailData = {}
this._bindEvent()
}
show(data) {
if (data.resTxt && trim(data.resTxt) === '') {
delete data.resTxt
}
if (isEmpty(data.resHeaders)) {
delete data.resHeaders
}
if (isEmpty(data.reqHeaders)) {
delete data.reqHeaders
}
let postData = ''
if (data.data) {
postData = `${escape(data.data)} `
}
let reqHeaders = 'Empty '
if (data.reqHeaders) {
reqHeaders = map(data.reqHeaders, (val, key) => {
return `
${escape(key)}
${escape(val)}
`
}).join('')
}
let resHeaders = 'Empty '
if (data.resHeaders) {
resHeaders = map(data.resHeaders, (val, key) => {
return `
${escape(key)}
${escape(val)}
`
}).join('')
}
let resTxt = ''
if (data.resTxt) {
let text = data.resTxt
if (text.length > MAX_RES_LEN) {
text = truncate(text, MAX_RES_LEN)
}
resTxt = `${escape(text)} `
}
const html = `
${escape(data.url)}
${postData}
Response Headers
Request Headers
${resTxt}
`
this._$container.html(html).show()
this._detailData = data
}
hide() {
this._$container.hide()
this.emit('hide')
}
_copyRes = () => {
const detailData = this._detailData
let data = `${detailData.method} ${detailData.url} ${detailData.status}\n`
if (!isEmpty(detailData.data)) {
data += '\nRequest Data\n\n'
data += `${detailData.data}\n`
}
if (!isEmpty(detailData.reqHeaders)) {
data += '\nRequest Headers\n\n'
each(detailData.reqHeaders, (val, key) => (data += `${key}: ${val}\n`))
}
if (!isEmpty(detailData.resHeaders)) {
data += '\nResponse Headers\n\n'
each(detailData.resHeaders, (val, key) => (data += `${key}: ${val}\n`))
}
if (detailData.resTxt) {
data += `\n${detailData.resTxt}\n`
}
copy(data)
this._devtools.notify('Copied', { icon: 'success' })
}
_bindEvent() {
const devtools = this._devtools
this._$container
.on('click', c('.back'), () => this.hide())
.on('click', c('.copy-res'), this._copyRes)
.on('click', c('.http .response'), () => {
const data = this._detailData
const resTxt = data.resTxt
if (isJson(resTxt)) {
return showSources('object', resTxt)
}
switch (data.subType) {
case 'css':
return showSources('css', resTxt)
case 'html':
return showSources('html', resTxt)
case 'javascript':
return showSources('js', resTxt)
case 'json':
return showSources('object', resTxt)
}
switch (data.type) {
case 'image':
return showSources('img', data.url)
}
})
const showSources = (type, data) => {
const sources = devtools.get('sources')
if (!sources) {
return
}
sources.set(type, data)
devtools.showTool('sources')
}
}
}
const MAX_RES_LEN = 100000
================================================
FILE: src/Network/Network.js
================================================
import Tool from '../DevTools/Tool'
import $ from 'licia/$'
import ms from 'licia/ms'
import each from 'licia/each'
import map from 'licia/map'
import Detail from './Detail'
import throttle from 'licia/throttle'
import { getFileName, classPrefix as c } from '../lib/util'
import evalCss from '../lib/evalCss'
import chobitsu from '../lib/chobitsu'
import emitter from '../lib/emitter'
import LunaDataGrid from 'luna-data-grid'
import ResizeSensor from 'licia/ResizeSensor'
import MediaQuery from 'licia/MediaQuery'
import { getType } from './util'
import copy from 'licia/copy'
import extend from 'licia/extend'
import trim from 'licia/trim'
import isNull from 'licia/isNull'
import LunaModal from 'luna-modal'
import { curlStr } from './util'
export default class Network extends Tool {
constructor() {
super()
this._style = evalCss(require('./Network.scss'))
this.name = 'network'
this._requests = {}
this._selectedRequest = null
this._isRecording = true
}
init($el, container) {
super.init($el)
this._container = container
this._initTpl()
this._detail = new Detail(this._$detail, container)
this._splitMediaQuery = new MediaQuery('screen and (min-width: 680px)')
this._splitMode = this._splitMediaQuery.isMatch()
this._requestDataGrid = new LunaDataGrid(this._$requests.get(0), {
columns: [
{
id: 'name',
title: 'Name',
sortable: true,
weight: 30,
},
{
id: 'method',
title: 'Method',
sortable: true,
weight: 14,
},
{
id: 'status',
title: 'Status',
sortable: true,
weight: 14,
},
{
id: 'type',
title: 'Type',
sortable: true,
weight: 14,
},
{
id: 'size',
title: 'Size',
sortable: true,
weight: 14,
},
{
id: 'time',
title: 'Time',
sortable: true,
weight: 14,
},
],
})
this._resizeSensor = new ResizeSensor($el.get(0))
this._bindEvent()
}
show() {
super.show()
this._updateDataGridHeight()
}
clear() {
this._requests = {}
this._requestDataGrid.clear()
}
requests() {
const ret = []
each(this._requests, (request) => {
ret.push(request)
})
return ret
}
_updateDataGridHeight() {
this._requestDataGrid.fit()
}
_reqWillBeSent = (params) => {
if (!this._isRecording) {
return
}
const request = {
name: getFileName(params.request.url),
url: params.request.url,
status: 'pending',
type: 'unknown',
subType: 'unknown',
size: 0,
data: params.request.postData,
method: params.request.method,
startTime: params.timestamp * 1000,
time: 0,
resTxt: '',
done: false,
reqHeaders: params.request.headers || {},
resHeaders: {},
}
let node
request.render = () => {
const data = {
name: request.name,
method: request.method,
status: request.status,
type: request.subType,
size: request.size,
time: request.displayTime,
}
if (node) {
node.data = data
node.render()
} else {
node = this._requestDataGrid.append(data, { selectable: true })
$(node.container).data('id', params.requestId)
}
if (request.hasErr) {
$(node.container).addClass(c('request-error'))
}
}
request.render()
this._requests[params.requestId] = request
}
_resReceivedExtraInfo = (params) => {
const request = this._requests[params.requestId]
if (!this._isRecording || !request) {
return
}
request.resHeaders = params.headers
this._updateType(request)
request.render()
}
_updateType(request) {
const contentType = request.resHeaders['content-type'] || ''
const { type, subType } = getType(contentType)
request.type = type
request.subType = subType
}
_resReceived = (params) => {
const request = this._requests[params.requestId]
if (!this._isRecording || !request) {
return
}
const { response } = params
const { status, headers } = response
request.status = status
if (status < 200 || status >= 300) {
request.hasErr = true
}
if (headers) {
request.resHeaders = headers
this._updateType(request)
}
request.render()
}
_loadingFinished = (params) => {
const request = this._requests[params.requestId]
if (!this._isRecording || !request) {
return
}
const time = params.timestamp * 1000
request.time = time - request.startTime
request.displayTime = ms(request.time)
request.size = params.encodedDataLength
request.done = true
request.resTxt = chobitsu.domain('Network').getResponseBody({
requestId: params.requestId,
}).body
request.render()
}
_loadingFailed = (params) => {
const request = this._requests[params.requestId]
if (!this._isRecording || !request) {
return
}
const time = params.timestamp * 1000
request.time = time - request.startTime
request.displayTime = ms(request.time)
request.hasErr = true
request.status = 0
request.done = true
request.render()
}
_copyCurl = () => {
const request = this._selectedRequest
copy(
curlStr({
requestMethod: request.method,
url() {
return request.url
},
requestFormData() {
return request.data
},
requestHeaders() {
const reqHeaders = request.reqHeaders || {}
extend(reqHeaders, {
'User-Agent': navigator.userAgent,
Referer: location.href,
})
return map(reqHeaders, (value, name) => {
return {
name,
value,
}
})
},
})
)
this._container.notify('Copied', { icon: 'success' })
}
_updateButtons() {
const $control = this._$control
const $showDetail = $control.find(c('.show-detail'))
const $copyCurl = $control.find(c('.copy-curl'))
const iconDisabled = c('icon-disabled')
$showDetail.addClass(iconDisabled)
$copyCurl.addClass(iconDisabled)
if (this._selectedRequest) {
$showDetail.rmClass(iconDisabled)
$copyCurl.rmClass(iconDisabled)
}
}
_toggleRecording = () => {
this._$control.find(c('.record')).toggleClass(c('recording'))
this._isRecording = !this._isRecording
}
_showDetail = () => {
if (this._selectedRequest) {
if (this._splitMode) {
this._$network.css('width', '50%')
}
this._detail.show(this._selectedRequest)
}
}
_bindEvent() {
const $control = this._$control
const $filterText = this._$filterText
const requestDataGrid = this._requestDataGrid
const self = this
$control
.on('click', c('.clear-request'), () => this.clear())
.on('click', c('.show-detail'), this._showDetail)
.on('click', c('.copy-curl'), this._copyCurl)
.on('click', c('.record'), this._toggleRecording)
.on('click', c('.filter'), () => {
LunaModal.prompt('Filter').then((filter) => {
if (isNull(filter)) return
$filterText.text(filter)
requestDataGrid.setOption('filter', trim(filter))
})
})
requestDataGrid.on('select', (node) => {
const id = $(node.container).data('id')
const request = self._requests[id]
this._selectedRequest = request
this._updateButtons()
if (this._splitMode) {
this._showDetail()
}
})
requestDataGrid.on('deselect', () => {
this._selectedRequest = null
this._updateButtons()
this._detail.hide()
})
this._resizeSensor.addListener(
throttle(() => this._updateDataGridHeight(), 15)
)
this._splitMediaQuery.on('match', () => {
this._detail.hide()
this._splitMode = true
})
this._splitMediaQuery.on('unmatch', () => {
this._detail.hide()
this._splitMode = false
})
this._detail.on('hide', () => {
if (this._splitMode) {
this._$network.css('width', '100%')
}
})
chobitsu.domain('Network').enable()
const network = chobitsu.domain('Network')
network.on('requestWillBeSent', this._reqWillBeSent)
network.on('responseReceivedExtraInfo', this._resReceivedExtraInfo)
network.on('responseReceived', this._resReceived)
network.on('loadingFinished', this._loadingFinished)
network.on('loadingFailed', this._loadingFailed)
emitter.on(emitter.SCALE, this._updateScale)
}
_updateScale = (scale) => {
this._splitMediaQuery.setQuery(`screen and (min-width: ${680 * scale}px)`)
}
destroy() {
super.destroy()
this._resizeSensor.destroy()
evalCss.remove(this._style)
this._splitMediaQuery.removeAllListeners()
const network = chobitsu.domain('Network')
network.off('requestWillBeSent', this._reqWillBeSent)
network.off('responseReceivedExtraInfo', this._resReceivedExtraInfo)
network.off('responseReceived', this._resReceived)
network.off('loadingFinished', this._loadingFinished)
emitter.off(emitter.SCALE, this._updateScale)
}
_initTpl() {
const $el = this._$el
$el.html(
c(`
`)
)
this._$network = $el.find(c('.network'))
this._$detail = $el.find(c('.detail'))
this._$requests = $el.find(c('.requests'))
this._$control = $el.find(c('.control'))
this._$filterText = $el.find(c('.filter-text'))
}
}
================================================
FILE: src/Network/Network.scss
================================================
@use '../style/variable' as *;
@use '../style/mixin' as *;
#network {
.network {
@include absolute();
padding-top: 39px;
}
.control {
padding: 10px;
border-bottom: none;
@include control();
.title {
font-size: $font-size;
}
.icon-clear {
left: 23px;
}
.icon-eye {
right: 0;
}
.icon-copy {
right: 23px;
}
.icon-filter {
right: 46px;
}
.filter-text {
white-space: nowrap;
position: absolute;
line-height: 20px;
max-width: 80px;
overflow: hidden;
right: 88px;
font-size: $font-size;
text-overflow: ellipsis;
}
.icon-record {
left: 0;
&.recording {
color: var(--console-error-foreground);
text-shadow: 0 0 4px var(--console-error-foreground);
}
}
}
.request-error {
color: var(--console-error-foreground);
}
.luna-data-grid:focus {
.luna-data-grid-data-container {
.request-error.luna-data-grid-selected {
background: var(--console-error-background);
}
}
}
.luna-data-grid {
border-left: none;
border-right: none;
}
.detail {
@include absolute();
z-index: 10;
display: none;
padding-top: 40px;
background: var(--background);
.control {
padding: 10px 35px;
border-bottom: 1px solid var(--border);
.url {
font-size: $font-size-s;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
display: inline-block;
}
.icon-left {
left: 0;
}
.icon-delete {
left: 0;
display: none;
}
.icon-copy {
right: 0;
}
}
.http {
@include overflow-auto(y);
height: 100%;
.section {
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
margin-top: 10px;
margin-bottom: 10px;
h2 {
background: var(--darker-background);
color: var(--primary);
padding: $padding;
line-height: 18px;
font-size: $font-size;
}
table {
color: var(--foreground);
* {
user-select: text;
}
td {
font-size: $font-size-s;
padding: 5px 10px;
word-break: break-all;
}
.key {
white-space: nowrap;
font-weight: bold;
color: var(--accent);
}
}
}
.response,
.data {
user-select: text;
@include overflow-auto(x);
padding: $padding;
font-size: $font-size-s;
margin: 10px 0;
white-space: pre-wrap;
border-top: 1px solid var(--border);
color: var(--foreground);
border-bottom: 1px solid var(--border);
}
}
}
}
.safe-area #network {
.http {
@include safe-area(padding-bottom, 0px);
}
}
@media screen and (min-width: 680px) {
#network {
.network {
.control {
.icon-eye {
display: none;
}
.icon-copy {
right: 0;
}
.icon-filter {
right: 23px;
}
.filter-text {
right: 55px;
}
}
}
.detail {
width: 50%;
left: initial;
right: 0;
border-left: 1px solid var(--border);
.control {
.icon-left {
display: none;
}
.icon-delete {
display: block;
}
}
}
}
}
================================================
FILE: src/Network/util.js
================================================
import last from 'licia/last'
import detectOs from 'licia/detectOs'
import arrToMap from 'licia/arrToMap'
export function getType(contentType) {
if (!contentType) return 'unknown'
const type = contentType.split(';')[0].split('/')
return {
type: type[0],
subType: last(type),
}
}
export function curlStr(request) {
let platform = detectOs()
if (platform === 'windows') {
platform = 'win'
}
let command = []
const ignoredHeaders = arrToMap([
'accept-encoding',
'host',
'method',
'path',
'scheme',
'version',
])
function escapeStringWin(str) {
const encapsChars = /[\r\n]/.test(str) ? '^"' : '"'
return (
encapsChars +
str
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/[^a-zA-Z0-9\s_\-:=+~'/.',?;()*`&]/g, '^$&')
.replace(/%(?=[a-zA-Z0-9_])/g, '%^')
.replace(/\r?\n/g, '^\n\n') +
encapsChars
)
}
function escapeStringPosix(str) {
function escapeCharacter(x) {
const code = x.charCodeAt(0)
let hexString = code.toString(16)
while (hexString.length < 4) {
hexString = '0' + hexString
}
return '\\u' + hexString
}
// eslint-disable-next-line no-control-regex
if (/[\0-\x1F\x7F-\x9F!]|'/.test(str)) {
return (
"$'" +
str
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
// eslint-disable-next-line no-control-regex
.replace(/[\0-\x1F\x7F-\x9F!]/g, escapeCharacter) +
"'"
)
}
return "'" + str + "'"
}
const escapeString = platform === 'win' ? escapeStringWin : escapeStringPosix
command.push(escapeString(request.url()).replace(/[[{}\]]/g, '\\$&'))
let inferredMethod = 'GET'
const data = []
const formData = request.requestFormData()
if (formData) {
data.push('--data-raw ' + escapeString(formData))
ignoredHeaders['content-length'] = true
inferredMethod = 'POST'
}
if (request.requestMethod !== inferredMethod) {
command.push('-X ' + escapeString(request.requestMethod))
}
const requestHeaders = request.requestHeaders()
for (let i = 0; i < requestHeaders.length; i++) {
const header = requestHeaders[i]
const name = header.name.replace(/^:/, '')
if (ignoredHeaders[name.toLowerCase()]) {
continue
}
command.push('-H ' + escapeString(name + ': ' + header.value))
}
command = command.concat(data)
command.push('--compressed')
return (
'curl ' +
command.join(
command.length >= 3 ? (platform === 'win' ? ' ^\n ' : ' \\\n ') : ' '
)
)
}
================================================
FILE: src/Resources/Cookie.js
================================================
import map from 'licia/map'
import trim from 'licia/trim'
import isNull from 'licia/isNull'
import each from 'licia/each'
import copy from 'licia/copy'
import LunaModal from 'luna-modal'
import LunaDataGrid from 'luna-data-grid'
import { setState, getState } from './util'
import chobitsu from '../lib/chobitsu'
import { classPrefix as c } from '../lib/util'
export default class Cookie {
constructor($container, devtools) {
this._$container = $container
this._devtools = devtools
this._selectedItem = null
this._initTpl()
this._dataGrid = new LunaDataGrid(this._$dataGrid.get(0), {
columns: [
{
id: 'key',
title: 'Key',
weight: 30,
},
{
id: 'value',
title: 'Value',
weight: 90,
},
],
minHeight: 60,
maxHeight: 223,
})
this._bindEvent()
}
refresh() {
const $container = this._$container
const dataGrid = this._dataGrid
const { cookies } = chobitsu.domain('Network').getCookies()
const cookieData = map(cookies, ({ name, value }) => ({
key: name,
val: value,
}))
dataGrid.clear()
each(cookieData, ({ key, val }) => {
dataGrid.append(
{
key,
value: val,
},
{
selectable: true,
}
)
})
const cookieState = getState('cookie', cookieData.length)
setState($container, cookieState)
}
_initTpl() {
const $container = this._$container
$container.html(
c(`
Cookie
`)
)
this._$dataGrid = $container.find(c('.data-grid'))
this._$filterText = $container.find(c('.filter-text'))
}
_updateButtons() {
const $container = this._$container
const $showDetail = $container.find(c('.show-detail'))
const $deleteCookie = $container.find(c('.delete-cookie'))
const $copyCookie = $container.find(c('.copy-cookie'))
const btnDisabled = c('btn-disabled')
$showDetail.addClass(btnDisabled)
$deleteCookie.addClass(btnDisabled)
$copyCookie.addClass(btnDisabled)
if (this._selectedItem) {
$showDetail.rmClass(btnDisabled)
$deleteCookie.rmClass(btnDisabled)
$copyCookie.rmClass(btnDisabled)
}
}
_getVal(key) {
const { cookies } = chobitsu.domain('Network').getCookies()
for (let i = 0, len = cookies.length; i < len; i++) {
if (cookies[i].name === key) {
return cookies[i].value
}
}
return ''
}
_bindEvent() {
const devtools = this._devtools
this._$container
.on('click', c('.refresh-cookie'), () => {
devtools.notify('Refreshed', { icon: 'success' })
this.refresh()
})
.on('click', c('.clear-cookie'), () => {
chobitsu.domain('Storage').clearDataForOrigin({
storageTypes: 'cookies',
})
this.refresh()
})
.on('click', c('.delete-cookie'), () => {
const key = this._selectedItem
chobitsu.domain('Network').deleteCookies({ name: key })
this.refresh()
})
.on('click', c('.show-detail'), () => {
const key = this._selectedItem
const val = this._getVal(key)
try {
showSources('object', JSON.parse(val))
} catch {
showSources('raw', val)
}
})
.on('click', c('.copy-cookie'), () => {
const key = this._selectedItem
copy(this._getVal(key))
devtools.notify('Copied', { icon: 'success' })
})
.on('click', c('.filter'), () => {
LunaModal.prompt('Filter').then((filter) => {
if (isNull(filter)) return
filter = trim(filter)
this._filter = filter
this._$filterText.text(filter)
this._dataGrid.setOption('filter', filter)
})
})
function showSources(type, data) {
const sources = devtools.get('sources')
if (!sources) return
sources.set(type, data)
devtools.showTool('sources')
return true
}
this._dataGrid
.on('select', (node) => {
this._selectedItem = node.data.key
this._updateButtons()
})
.on('deselect', () => {
this._selectedItem = null
this._updateButtons()
})
}
}
================================================
FILE: src/Resources/Resources.js
================================================
import Tool from '../DevTools/Tool'
import Settings from '../Settings/Settings'
import $ from 'licia/$'
import escape from 'licia/escape'
import isEmpty from 'licia/isEmpty'
import contain from 'licia/contain'
import unique from 'licia/unique'
import each from 'licia/each'
import sameOrigin from 'licia/sameOrigin'
import ajax from 'licia/ajax'
import MutationObserver from 'licia/MutationObserver'
import toArr from 'licia/toArr'
import concat from 'licia/concat'
import map from 'licia/map'
import { isErudaEl, classPrefix as c } from '../lib/util'
import evalCss from '../lib/evalCss'
import Storage from './Storage'
import Cookie from './Cookie'
import { setState, getState } from './util'
export default class Resources extends Tool {
constructor() {
super()
this._style = evalCss(require('./Resources.scss'))
this.name = 'resources'
this._hideErudaSetting = false
this._observeElement = true
}
init($el, container) {
super.init($el)
this._container = container
this._initTpl()
this._localStorage = new Storage(
this._$localStorage,
container,
this,
'local'
)
this._sessionStorage = new Storage(
this._$sessionStorage,
container,
this,
'session'
)
this._cookie = new Cookie(this._$cookie, container)
this._bindEvent()
this._initObserver()
this._initCfg()
}
refresh() {
return this.refreshLocalStorage()
.refreshSessionStorage()
.refreshCookie()
.refreshScript()
.refreshStylesheet()
.refreshIframe()
.refreshImage()
}
destroy() {
super.destroy()
this._localStorage.destroy()
this._sessionStorage.destroy()
this._disableObserver()
evalCss.remove(this._style)
this._rmCfg()
}
refreshScript() {
let scriptData = []
$('script').each(function () {
const src = this.src
if (src !== '') scriptData.push(src)
})
scriptData = unique(scriptData)
const scriptState = getState('script', scriptData.length)
let scriptDataHtml = 'Empty '
if (!isEmpty(scriptData)) {
scriptDataHtml = map(scriptData, (script) => {
script = escape(script)
return `${script} `
}).join('')
}
const scriptHtml = `
Script
`
const $script = this._$script
setState($script, scriptState)
$script.html(scriptHtml)
return this
}
refreshStylesheet() {
let stylesheetData = []
$('link').each(function () {
if (this.rel !== 'stylesheet') return
stylesheetData.push(this.href)
})
stylesheetData = unique(stylesheetData)
const stylesheetState = getState('stylesheet', stylesheetData.length)
let stylesheetDataHtml = 'Empty '
if (!isEmpty(stylesheetData)) {
stylesheetDataHtml = map(stylesheetData, (stylesheet) => {
stylesheet = escape(stylesheet)
return ` ${stylesheet} `
}).join('')
}
const stylesheetHtml = `
Stylesheet
`
const $stylesheet = this._$stylesheet
setState($stylesheet, stylesheetState)
$stylesheet.html(stylesheetHtml)
return this
}
refreshIframe() {
let iframeData = []
$('iframe').each(function () {
const $this = $(this)
const src = $this.attr('src')
if (src) iframeData.push(src)
})
iframeData = unique(iframeData)
let iframeDataHtml = 'Empty '
if (!isEmpty(iframeData)) {
iframeDataHtml = map(iframeData, (iframe) => {
iframe = escape(iframe)
return `${iframe} `
}).join('')
}
const iframeHtml = `
Iframe
`
this._$iframe.html(iframeHtml)
return this
}
refreshLocalStorage() {
this._localStorage.refresh()
return this
}
refreshSessionStorage() {
this._sessionStorage.refresh()
return this
}
refreshCookie() {
this._cookie.refresh()
return this
}
refreshImage() {
let imageData = []
const performance = (this._performance =
window.webkitPerformance || window.performance)
if (performance && performance.getEntries) {
const entries = this._performance.getEntries()
entries.forEach((entry) => {
if (entry.initiatorType === 'img' || isImg(entry.name)) {
if (contain(entry.name, 'exclude=true')) {
return
}
imageData.push(entry.name)
}
})
} else {
$('img').each(function () {
const $this = $(this)
const src = $this.attr('src')
if ($this.data('exclude') === 'true') {
return
}
imageData.push(src)
})
}
imageData = unique(imageData)
imageData.sort()
const imageState = getState('image', imageData.length)
let imageDataHtml = 'Empty '
if (!isEmpty(imageData)) {
// prettier-ignore
imageDataHtml = map(imageData, (image) => {
return `
`
}).join('')
}
const imageHtml = `
Image
`
const $image = this._$image
setState($image, imageState)
$image.html(imageHtml)
return this
}
show() {
super.show()
if (this._observeElement) this._enableObserver()
return this.refresh()
}
hide() {
this._disableObserver()
return super.hide()
}
_initTpl() {
const $el = this._$el
$el.html(
c(`
`)
)
this._$localStorage = $el.find(c('.local-storage'))
this._$sessionStorage = $el.find(c('.session-storage'))
this._$cookie = $el.find(c('.cookie'))
this._$script = $el.find(c('.script'))
this._$stylesheet = $el.find(c('.stylesheet'))
this._$iframe = $el.find(c('.iframe'))
this._$image = $el.find(c('.image'))
}
_bindEvent() {
const $el = this._$el
const container = this._container
$el
.on('click', '.eruda-refresh-script', () => {
container.notify('Refreshed', { icon: 'success' })
this.refreshScript()
})
.on('click', '.eruda-refresh-stylesheet', () => {
container.notify('Refreshed', { icon: 'success' })
this.refreshStylesheet()
})
.on('click', '.eruda-refresh-iframe', () => {
container.notify('Refreshed', { icon: 'success' })
this.refreshIframe()
})
.on('click', '.eruda-refresh-image', () => {
container.notify('Refreshed', { icon: 'success' })
this.refreshImage()
})
.on('click', '.eruda-img-link', function () {
const src = $(this).attr('src')
showSources('img', src)
})
.on('click', '.eruda-css-link', linkFactory('css'))
.on('click', '.eruda-js-link', linkFactory('js'))
.on('click', '.eruda-iframe-link', linkFactory('iframe'))
function showSources(type, data) {
const sources = container.get('sources')
if (!sources) return
sources.set(type, data)
container.showTool('sources')
return true
}
function linkFactory(type) {
return function (e) {
if (!container.get('sources')) return
e.preventDefault()
const url = $(this).attr('href')
if (type === 'iframe' || !sameOrigin(location.href, url)) {
showSources('iframe', url)
} else {
ajax({
url,
success: (data) => {
showSources(type, data)
},
dataType: 'raw',
})
}
}
}
}
_rmCfg() {
const cfg = this.config
const settings = this._container.get('settings')
if (!settings) return
settings
.remove(cfg, 'hideErudaSetting')
.remove(cfg, 'observeElement')
.remove('Resources')
}
_initCfg() {
const cfg = (this.config = Settings.createCfg('resources', {
hideErudaSetting: true,
observeElement: true,
}))
if (cfg.get('hideErudaSetting')) this._hideErudaSetting = true
if (!cfg.get('observeElement')) this._observeElement = false
cfg.on('change', (key, val) => {
switch (key) {
case 'hideErudaSetting':
this._hideErudaSetting = val
return
case 'observeElement':
this._observeElement = val
return val ? this._enableObserver() : this._disableObserver()
}
})
const settings = this._container.get('settings')
settings
.text('Resources')
.switch(cfg, 'hideErudaSetting', 'Hide Eruda Setting')
.switch(cfg, 'observeElement', 'Auto Refresh Elements')
.separator()
}
_initObserver() {
this._observer = new MutationObserver((mutations) => {
each(mutations, (mutation) => {
this._handleMutation(mutation)
})
})
}
_handleMutation(mutation) {
if (isErudaEl(mutation.target)) return
const checkEl = (el) => {
const tagName = getLowerCaseTagName(el)
switch (tagName) {
case 'script':
this.refreshScript()
break
case 'img':
this.refreshImage()
break
case 'link':
this.refreshStylesheet()
break
}
}
if (mutation.type === 'attributes') {
checkEl(mutation.target)
} else if (mutation.type === 'childList') {
checkEl(mutation.target)
let nodes = toArr(mutation.addedNodes)
nodes = concat(nodes, toArr(mutation.removedNodes))
for (const node of nodes) {
checkEl(node)
}
}
}
_enableObserver() {
this._observer.observe(document.documentElement, {
attributes: true,
childList: true,
subtree: true,
})
}
_disableObserver() {
this._observer.disconnect()
}
}
function getLowerCaseTagName(el) {
if (!el.tagName) return ''
return el.tagName.toLowerCase()
}
const regImg = /\.(jpeg|jpg|gif|png)$/
const isImg = (url) => regImg.test(url)
================================================
FILE: src/Resources/Resources.scss
================================================
@use '../style/variable' as *;
@use '../style/mixin' as *;
#resources {
@include overflow-auto(y);
padding: 10px;
font-size: 14px;
.section {
margin-bottom: 10px;
overflow: hidden;
border: 1px solid var(--border);
&.warn {
border: 1px solid var(--console-warn-border);
.title {
background: var(--console-warn-background);
color: var(--console-warn-foreground);
}
}
&.danger {
border: 1px solid var(--console-error-border);
.title {
background: var(--console-error-background);
color: var(--console-error-foreground);
}
}
&.local-storage,
&.session-storage,
&.cookie {
border: none;
.title {
border: 1px solid var(--border);
border-bottom: none;
}
}
}
.title {
padding: $padding;
line-height: 18px;
color: var(--primary);
background: var(--darker-background);
@include right-btn();
}
.link-list {
font-size: $font-size-s;
color: var(--foreground);
li {
padding: 10px;
word-break: break-all;
a {
color: var(--link-color) !important;
}
}
}
.image-list {
color: var(--foreground);
font-size: $font-size-s;
display: flex;
flex-wrap: wrap;
padding-left: $padding;
padding-top: $padding;
&::after {
content: '';
flex-grow: 1000;
}
li {
flex-grow: 1;
cursor: pointer;
overflow-y: hidden;
margin-right: $padding;
margin-bottom: $padding;
border: 1px solid var(--border);
&.image {
height: 100px;
font-size: 0;
}
img {
height: 100px;
min-width: 100%;
object-fit: cover;
}
}
}
}
.safe-area #resources {
@include safe-area(padding-bottom, 10px);
}
================================================
FILE: src/Resources/Storage.js
================================================
import each from 'licia/each'
import isStr from 'licia/isStr'
import startWith from 'licia/startWith'
import truncate from 'licia/truncate'
import LunaModal from 'luna-modal'
import LunaDataGrid from 'luna-data-grid'
import isNull from 'licia/isNull'
import trim from 'licia/trim'
import copy from 'licia/copy'
import emitter from '../lib/emitter'
import { safeStorage, classPrefix as c } from '../lib/util'
export default class Storage {
constructor($container, devtools, resources, type) {
this._type = type
this._$container = $container
this._devtools = devtools
this._resources = resources
this._selectedItem = null
this._storeData = []
this._initTpl()
this._dataGrid = new LunaDataGrid(this._$dataGrid.get(0), {
columns: [
{
id: 'key',
title: 'Key',
weight: 30,
},
{
id: 'value',
title: 'Value',
weight: 90,
},
],
minHeight: 60,
maxHeight: 223,
})
this._bindEvent()
}
destroy() {
emitter.off(emitter.SCALE, this._updateGridHeight)
}
refresh() {
const dataGrid = this._dataGrid
this._refreshStorage()
dataGrid.clear()
each(this._storeData, ({ key, val }) => {
dataGrid.append(
{
key,
value: val,
},
{
selectable: true,
}
)
})
}
_refreshStorage() {
const resources = this._resources
let store = safeStorage(this._type, false)
if (!store) return
const storeData = []
// Mobile safari is not able to loop through localStorage directly.
store = JSON.parse(JSON.stringify(store))
each(store, (val, key) => {
// According to issue 20, not all values are guaranteed to be string.
if (!isStr(val)) return
if (resources.config.get('hideErudaSetting')) {
if (startWith(key, 'eruda') || key === 'active-eruda') return
}
storeData.push({
key: key,
val: truncate(val, 200),
})
})
this._storeData = storeData
}
_updateButtons() {
const $container = this._$container
const $showDetail = $container.find(c('.show-detail'))
const $deleteStorage = $container.find(c('.delete-storage'))
const $copyStorage = $container.find(c('.copy-storage'))
const btnDisabled = c('btn-disabled')
$showDetail.addClass(btnDisabled)
$deleteStorage.addClass(btnDisabled)
$copyStorage.addClass(btnDisabled)
if (this._selectedItem) {
$showDetail.rmClass(btnDisabled)
$deleteStorage.rmClass(btnDisabled)
$copyStorage.rmClass(btnDisabled)
}
}
_initTpl() {
const $container = this._$container
const type = this._type
$container.html(
c(`
${type === 'local' ? 'Local' : 'Session'} Storage
`)
)
this._$dataGrid = $container.find(c('.data-grid'))
this._$filterText = $container.find(c('.filter-text'))
}
_getVal(key) {
return this._type === 'local'
? localStorage.getItem(key)
: sessionStorage.getItem(key)
}
_updateGridHeight = (scale) => {
this._dataGrid.setOption({
minHeight: 60 * scale,
maxHeight: 223 * scale,
})
}
_bindEvent() {
const type = this._type
const devtools = this._devtools
this._$container
.on('click', c('.refresh-storage'), () => {
devtools.notify('Refreshed', { icon: 'success' })
this.refresh()
})
.on('click', c('.clear-storage'), () => {
each(this._storeData, (val) => {
if (type === 'local') {
localStorage.removeItem(val.key)
} else {
sessionStorage.removeItem(val.key)
}
})
this.refresh()
})
.on('click', c('.show-detail'), () => {
const key = this._selectedItem
const val = this._getVal(key)
try {
showSources('object', JSON.parse(val))
} catch {
showSources('raw', val)
}
})
.on('click', c('.copy-storage'), () => {
const key = this._selectedItem
copy(this._getVal(key))
devtools.notify('Copied', { icon: 'success' })
})
.on('click', c('.filter'), () => {
LunaModal.prompt('Filter').then((filter) => {
if (isNull(filter)) return
filter = trim(filter)
this._$filterText.text(filter)
this._dataGrid.setOption('filter', filter)
})
})
.on('click', c('.delete-storage'), () => {
const key = this._selectedItem
if (type === 'local') {
localStorage.removeItem(key)
} else {
sessionStorage.removeItem(key)
}
this.refresh()
})
function showSources(type, data) {
const sources = devtools.get('sources')
if (!sources) return
sources.set(type, data)
devtools.showTool('sources')
return true
}
this._dataGrid
.on('select', (node) => {
this._selectedItem = node.data.key
this._updateButtons()
})
.on('deselect', () => {
this._selectedItem = null
this._updateButtons()
})
emitter.on(emitter.SCALE, this._updateGridHeight)
}
}
================================================
FILE: src/Resources/util.js
================================================
import { classPrefix as c } from '../lib/util'
export function setState($el, state) {
$el
.rmClass(c('ok'))
.rmClass(c('danger'))
.rmClass(c('warn'))
.addClass(c(state))
}
export function getState(type, len) {
if (len === 0) return ''
let warn = 0
let danger = 0
switch (type) {
case 'cookie':
warn = 30
danger = 60
break
case 'script':
warn = 5
danger = 10
break
case 'stylesheet':
warn = 4
danger = 8
break
case 'image':
warn = 50
danger = 100
break
}
if (len >= danger) return 'danger'
if (len >= warn) return 'warn'
return 'ok'
}
================================================
FILE: src/Settings/Settings.js
================================================
import Tool from '../DevTools/Tool'
import $ from 'licia/$'
import LocalStore from 'licia/LocalStore'
import uniqId from 'licia/uniqId'
import each from 'licia/each'
import filter from 'licia/filter'
import isStr from 'licia/isStr'
import contain from 'licia/contain'
import clone from 'licia/clone'
import evalCss from '../lib/evalCss'
import LunaSetting from 'luna-setting'
export default class Settings extends Tool {
constructor() {
super()
this._style = evalCss(require('./Settings.scss'))
this.name = 'settings'
this._settings = []
}
init($el) {
super.init($el)
this._setting = new LunaSetting($el.get(0))
this._bindEvent()
}
remove(config, key) {
if (isStr(config)) {
const self = this
this._$el.find('.luna-setting-item-title').each(function () {
const $this = $(this)
if ($this.text() === config) {
self._setting.remove(this.settingItem)
}
})
} else {
this._settings = filter(this._settings, (setting) => {
if (setting.config === config && setting.key === key) {
this._setting.remove(setting.item)
return false
}
return true
})
}
this._cleanSeparator()
return this
}
destroy() {
this._setting.destroy()
super.destroy()
evalCss.remove(this._style)
}
clear() {
this._settings = []
this._setting.clear()
}
switch(config, key, desc) {
const id = this._genId()
const item = this._setting.appendCheckbox(id, !!config.get(key), desc)
this._settings.push({ config, key, id, item })
return this
}
select(config, key, desc, selections) {
const id = this._genId()
const selectOptions = {}
each(selections, (selection) => (selectOptions[selection] = selection))
const item = this._setting.appendSelect(
id,
config.get(key),
'',
desc,
selectOptions
)
this._settings.push({ config, key, id, item })
return this
}
range(config, key, desc, { min = 0, max = 1, step = 0.1 }) {
const id = this._genId()
const item = this._setting.appendNumber(id, config.get(key), desc, {
max,
min,
step,
range: true,
})
this._settings.push({ config, key, min, max, step, id, item })
return this
}
button(text, handler) {
this._setting.appendButton(text, handler)
return this
}
separator() {
this._setting.appendSeparator()
return this
}
text(text) {
this._setting.appendTitle(text)
return this
}
// Merge adjacent separators
_cleanSeparator() {
const children = clone(this._$el.get(0).children)
function isSeparator(node) {
return contain(node.getAttribute('class'), 'luna-setting-item-separator')
}
for (let i = 0, len = children.length; i < len - 1; i++) {
if (isSeparator(children[i]) && isSeparator(children[i + 1])) {
$(children[i]).remove()
}
}
}
_genId() {
return uniqId('eruda-settings')
}
_getSetting(id) {
let ret
each(this._settings, (setting) => {
if (setting.id === id) ret = setting
})
return ret
}
_bindEvent() {
this._setting.on('change', (id, val) => {
const setting = this._getSetting(id)
setting.config.set(setting.key, val)
})
}
static createCfg(name, data) {
return new LocalStore('eruda-' + name, data)
}
}
================================================
FILE: src/Settings/Settings.scss
================================================
@use '../style/variable' as *;
@use '../style/mixin' as *;
#settings {
@include overflow-auto(y);
}
.safe-area #settings {
@include safe-area(padding-bottom, 0px);
}
================================================
FILE: src/Snippets/Snippets.js
================================================
import Tool from '../DevTools/Tool'
import defSnippets from './defSnippets'
import $ from 'licia/$'
import each from 'licia/each'
import escape from 'licia/escape'
import map from 'licia/map'
import remove from 'licia/remove'
import evalCss from '../lib/evalCss'
import { classPrefix as c } from '../lib/util'
export default class Snippets extends Tool {
constructor() {
super()
this._style = evalCss(require('./Snippets.scss'))
this.name = 'snippets'
this._snippets = []
}
init($el) {
super.init($el)
this._bindEvent()
this._addDefSnippets()
}
destroy() {
super.destroy()
evalCss.remove(this._style)
}
add(name, fn, desc) {
this._snippets.push({ name, fn, desc })
this._render()
return this
}
remove(name) {
remove(this._snippets, (snippet) => snippet.name === name)
this._render()
return this
}
run(name) {
const snippets = this._snippets
for (let i = 0, len = snippets.length; i < len; i++) {
if (snippets[i].name === name) this._run(i)
}
return this
}
clear() {
this._snippets = []
this._render()
return this
}
_bindEvent() {
const self = this
this._$el.on('click', '.eruda-run', function () {
const idx = $(this).data('idx')
self._run(idx)
})
}
_run(idx) {
this._snippets[idx].fn.call(null)
}
_addDefSnippets() {
each(defSnippets, (snippet) => {
this.add(snippet.name, snippet.fn, snippet.desc)
})
}
_render() {
const html = map(this._snippets, (snippet, idx) => {
return `
${escape(snippet.name)}
${escape(snippet.desc)}
`
}).join('')
this._renderHtml(html)
}
_renderHtml(html) {
if (html === this._lastHtml) return
this._lastHtml = html
this._$el.html(html)
}
}
================================================
FILE: src/Snippets/Snippets.scss
================================================
@use '../style/variable' as *;
@use '../style/mixin' as *;
#snippets {
@include overflow-auto(y);
padding: $padding;
.section {
margin-bottom: 10px;
border: 1px solid var(--border);
overflow: hidden;
cursor: pointer;
&:active {
.name {
background: var(--highlight);
color: var(--select-foreground);
}
}
.name {
padding: $padding;
line-height: 18px;
color: var(--primary);
background: var(--darker-background);
transition: background-color $anim-duration;
.btn {
margin-left: 10px;
float: right;
text-align: center;
width: 18px;
height: 18px;
font-size: $font-size-s;
}
}
.description {
font-size: $font-size-s;
color: var(--foreground);
padding: $padding;
transition: background-color $anim-duration;
}
}
}
.safe-area #snippets {
@include safe-area(padding-bottom, 10px);
}
================================================
FILE: src/Snippets/defSnippets.js
================================================
import logger from '../lib/logger'
import emitter from '../lib/emitter'
import Url from 'licia/Url'
import now from 'licia/now'
import startWith from 'licia/startWith'
import $ from 'licia/$'
import upperFirst from 'licia/upperFirst'
import loadJs from 'licia/loadJs'
import trim from 'licia/trim'
import LunaModal from 'luna-modal'
import { isErudaEl } from '../lib/util'
import evalCss from '../lib/evalCss'
let style = null
export default [
{
name: 'Border All',
fn() {
if (style) {
evalCss.remove(style)
style = null
return
}
style = evalCss(
'* { outline: 2px dashed #707d8b; outline-offset: -3px; }',
document.head
)
},
desc: 'Add color borders to all elements',
},
{
name: 'Refresh Page',
fn() {
const url = new Url()
url.setQuery('timestamp', now())
window.location.replace(url.toString())
},
desc: 'Add timestamp to url and refresh',
},
{
name: 'Search Text',
fn() {
LunaModal.prompt('Enter the text').then((keyword) => {
if (!keyword || trim(keyword) === '') {
return
}
search(keyword)
})
},
desc: 'Highlight given text on page',
},
{
name: 'Edit Page',
fn() {
const body = document.body
body.contentEditable = body.contentEditable !== 'true'
},
desc: 'Toggle body contentEditable',
},
{
name: 'Fit Screen',
// https://achrafkassioui.com/birdview/
fn() {
const body = document.body
const html = document.documentElement
const $body = $(body)
if ($body.data('scaled')) {
window.scrollTo(0, +$body.data('scaled'))
$body.rmAttr('data-scaled')
$body.css('transform', 'none')
} else {
const documentHeight = Math.max(
body.scrollHeight,
body.offsetHeight,
html.clientHeight,
html.scrollHeight,
html.offsetHeight
)
const viewportHeight = Math.max(
document.documentElement.clientHeight,
window.innerHeight || 0
)
const scaleVal = viewportHeight / documentHeight
$body.css('transform', `scale(${scaleVal})`)
$body.data('scaled', window.scrollY)
window.scrollTo(0, documentHeight / 2 - viewportHeight / 2)
}
},
desc: 'Scale down the whole page to fit screen',
},
{
name: 'Load Vue Plugin',
fn() {
loadPlugin('vue')
},
desc: 'Vue devtools',
},
{
name: 'Load Monitor Plugin',
fn() {
loadPlugin('monitor')
},
desc: 'Display page fps, memory and dom nodes',
},
{
name: 'Load Features Plugin',
fn() {
loadPlugin('features')
},
desc: 'Browser feature detections',
},
{
name: 'Load Timing Plugin',
fn() {
loadPlugin('timing')
},
desc: 'Show performance and resource timing',
},
{
name: 'Load Code Plugin',
fn() {
loadPlugin('code')
},
desc: 'Edit and run JavaScript',
},
{
name: 'Load Benchmark Plugin',
fn() {
loadPlugin('benchmark')
},
desc: 'Run JavaScript benchmarks',
},
{
name: 'Load Geolocation Plugin',
fn() {
loadPlugin('geolocation')
},
desc: 'Test geolocation',
},
{
name: 'Load Orientation Plugin',
fn() {
loadPlugin('orientation')
},
desc: 'Test orientation api',
},
{
name: 'Load Touches Plugin',
fn() {
loadPlugin('touches')
},
desc: 'Visualize screen touches',
},
]
evalCss(require('./searchText.scss'), document.head)
function search(text) {
const root = document.body
const regText = new RegExp(text, 'ig')
traverse(root, (node) => {
const $node = $(node)
if (!$node.hasClass('eruda-search-highlight-block')) return
return document.createTextNode($node.text())
})
traverse(root, (node) => {
if (node.nodeType !== 3) return
let val = node.nodeValue
val = val.replace(
regText,
(match) => `${match} `
)
if (val === node.nodeValue) return
const $ret = $(document.createElement('div'))
$ret.html(val)
$ret.addClass('eruda-search-highlight-block')
return $ret.get(0)
})
}
function traverse(root, processor) {
const childNodes = root.childNodes
if (isErudaEl(root)) return
for (let i = 0, len = childNodes.length; i < len; i++) {
const newNode = traverse(childNodes[i], processor)
if (newNode) root.replaceChild(newNode, childNodes[i])
}
return processor(root)
}
function loadPlugin(name) {
const globalName = 'eruda' + upperFirst(name)
if (window[globalName]) return
let protocol = location.protocol
if (!startWith(protocol, 'http')) protocol = 'http:'
loadJs(
`${protocol}//cdn.jsdelivr.net/npm/eruda-${name}@${pluginVersion[name]}`,
(isLoaded) => {
if (!isLoaded || !window[globalName])
return logger.error('Fail to load plugin ' + name)
emitter.emit(emitter.ADD, window[globalName])
emitter.emit(emitter.SHOW, name)
}
)
}
const pluginVersion = {
monitor: '1.1.1',
features: '2.1.0',
timing: '2.0.1',
code: '2.2.0',
benchmark: '2.0.1',
geolocation: '2.1.0',
orientation: '2.1.1',
touches: '2.1.0',
vue: '1.1.1',
}
================================================
FILE: src/Snippets/searchText.scss
================================================
@use '../style/variable' as *;
.search-highlight-block {
display: inline;
.keyword {
background: var(--console-warn-background);
color: var(--console-warn-foreground);
}
}
================================================
FILE: src/Sources/Sources.js
================================================
import Tool from '../DevTools/Tool'
import LunaObjectViewer from 'luna-object-viewer'
import Settings from '../Settings/Settings'
import ajax from 'licia/ajax'
import each from 'licia/each'
import isStr from 'licia/isStr'
import escape from 'licia/escape'
import truncate from 'licia/truncate'
import replaceAll from 'licia/replaceAll'
import highlight from 'licia/highlight'
import LunaTextViewer from 'luna-text-viewer'
import evalCss from '../lib/evalCss'
import { classPrefix as c } from '../lib/util'
export default class Sources extends Tool {
constructor() {
super()
this._style = evalCss(require('./Sources.scss'))
this.name = 'sources'
this._showLineNum = true
}
init($el, container) {
super.init($el)
this._container = container
this._bindEvent()
this._initCfg()
}
destroy() {
super.destroy()
evalCss.remove(this._style)
this._rmCfg()
}
set(type, val) {
if (type === 'img') {
this._isFetchingData = true
const img = new Image()
const self = this
img.onload = function () {
self._isFetchingData = false
self._data = {
type: 'img',
val: {
width: this.width,
height: this.height,
src: val,
},
}
self._render()
}
img.onerror = function () {
self._isFetchingData = false
}
img.src = val
return
}
this._data = { type, val }
this._render()
return this
}
show() {
super.show()
if (!this._data && !this._isFetchingData) {
this._renderDef()
}
return this
}
_renderDef() {
if (this._html) {
this._data = {
type: 'html',
val: this._html,
}
return this._render()
}
if (this._isGettingHtml) return
this._isGettingHtml = true
ajax({
url: location.href,
success: (data) => (this._html = data),
error: () => (this._html = 'Sorry, unable to fetch source code:('),
complete: () => {
this._isGettingHtml = false
this._renderDef()
},
dataType: 'raw',
})
}
_bindEvent() {
this._container.on('showTool', (name, lastTool) => {
if (name !== this.name && lastTool.name === this.name) {
delete this._data
}
})
}
_rmCfg() {
const cfg = this.config
const settings = this._container.get('settings')
if (!settings) return
settings.remove(cfg, 'showLineNum').remove('Sources')
}
_initCfg() {
const cfg = (this.config = Settings.createCfg('sources', {
showLineNum: true,
}))
if (!cfg.get('showLineNum')) this._showLineNum = false
cfg.on('change', (key, val) => {
switch (key) {
case 'showLineNum':
this._showLineNum = val
return
}
})
const settings = this._container.get('settings')
settings
.text('Sources')
.switch(cfg, 'showLineNum', 'Show Line Numbers')
.separator()
}
_render() {
this._isInit = true
const data = this._data
switch (data.type) {
case 'html':
case 'js':
case 'css':
return this._renderCode()
case 'img':
return this._renderImg()
case 'object':
return this._renderObj()
case 'raw':
return this._renderRaw()
case 'iframe':
return this._renderIframe()
}
}
_renderImg() {
const { width, height, src } = this._data.val
this._renderHtml(`
${escape(src)}
${escape(width)} × ${escape(height)}
`)
}
_renderCode() {
const data = this._data
this._renderHtml(
`
`,
false
)
let code = data.val
const len = data.val.length
if (len > MAX_RAW_LEN) {
code = truncate(code, MAX_RAW_LEN)
}
// If source code too big, don't process it.
if (len < MAX_BEAUTIFY_LEN) {
code = highlight(code, data.type, {
comment: '',
string: '',
number: '',
keyword: '',
operator: '',
})
each(['comment', 'string', 'number', 'keyword', 'operator'], (type) => {
code = replaceAll(code, `class="${type}"`, `class="${c(type)}"`)
})
} else {
code = escape(code)
}
const container = this._$el.find(c('.code')).get(0)
new LunaTextViewer(container, {
text: code,
escape: false,
wrapLongLines: true,
showLineNumbers: data.val.length < MAX_LINE_NUM_LEN && this._showLineNum,
})
}
_renderObj() {
// Using cache will keep binding events to the same elements.
this._renderHtml(``, false)
let val = this._data.val
try {
if (isStr(val)) {
val = JSON.parse(val)
}
} catch {
// No op
}
const objViewer = new LunaObjectViewer(
this._$el.find('.eruda-json').get(0),
{
unenumerable: true,
accessGetter: true,
prototype: false,
}
)
objViewer.set(val)
}
_renderRaw() {
const data = this._data
this._renderHtml(``)
let val = data.val
const container = this._$el.find(c('.raw')).get(0)
if (val.length > MAX_RAW_LEN) {
val = truncate(val, MAX_RAW_LEN)
}
new LunaTextViewer(container, {
text: val,
wrapLongLines: true,
showLineNumbers: val.length < MAX_LINE_NUM_LEN && this._showLineNum,
})
}
_renderIframe() {
this._renderHtml(``)
}
_renderHtml(html, cache = true) {
if (cache && html === this._lastHtml) return
this._lastHtml = html
this._$el.html(html)
// Need setTimeout to make it work
setTimeout(() => (this._$el.get(0).scrollTop = 0), 0)
}
}
const MAX_BEAUTIFY_LEN = 30000
const MAX_LINE_NUM_LEN = 80000
const MAX_RAW_LEN = 100000
================================================
FILE: src/Sources/Sources.scss
================================================
@use '../style/variable' as *;
@use '../style/mixin' as *;
#sources {
font-size: 0;
@include overflow-auto(y);
color: var(--foreground);
.code-wrapper,
.raw-wrapper {
@include overflow-auto(x);
width: 100%;
min-height: 100%;
}
.raw,
.code {
height: 100%;
.keyword {
color: var(--keyword-color);
}
.comment {
color: var(--comment-color);
}
.number {
color: var(--number-color);
}
.string {
color: var(--string-color);
}
.operator {
color: var(--operator-color);
}
&[data-type='html'] {
.keyword {
color: var(--tag-name-color);
}
}
}
.image {
font-size: $font-size-s;
.breadcrumb {
@include breadcrumb();
}
.img-container {
text-align: center;
img {
max-width: 100%;
}
}
.img-info {
text-align: center;
margin: 20px 0;
color: var(--foreground);
}
}
.json {
padding: 0 $padding;
* {
user-select: text;
}
}
iframe {
width: 100%;
height: 100%;
}
}
================================================
FILE: src/eruda.js
================================================
import EntryBtn from './EntryBtn/EntryBtn'
import DevTools from './DevTools/DevTools'
import Tool from './DevTools/Tool'
import Console from './Console/Console'
import Network from './Network/Network'
import Elements from './Elements/Elements'
import Snippets from './Snippets/Snippets'
import Resources from './Resources/Resources'
import Info from './Info/Info'
import Sources from './Sources/Sources'
import Settings from './Settings/Settings'
import emitter from './lib/emitter'
import logger from './lib/logger'
import * as util from './lib/util'
import { isDarkTheme } from './lib/themes'
import themes from './lib/themes'
import isFn from 'licia/isFn'
import isNum from 'licia/isNum'
import isObj from 'licia/isObj'
import each from 'licia/each'
import isMobile from 'licia/isMobile'
import viewportScale from 'licia/viewportScale'
import detectBrowser from 'licia/detectBrowser'
import $ from 'licia/$'
import toArr from 'licia/toArr'
import upperFirst from 'licia/upperFirst'
import nextTick from 'licia/nextTick'
import isEqual from 'licia/isEqual'
import extend from 'licia/extend'
import evalCss from './lib/evalCss'
import chobitsu from './lib/chobitsu'
export default {
init({
container,
tool,
autoScale = true,
useShadowDom = true,
inline = false,
defaults = {},
} = {}) {
if (this._isInit) {
return
}
this._isInit = true
this._scale = 1
this._initContainer(container, useShadowDom)
this._initStyle()
this._initDevTools(defaults, inline)
this._initEntryBtn()
this._initSettings()
this._initTools(tool)
this._registerListener()
if (autoScale) {
this._autoScale()
}
if (inline) {
this._entryBtn.hide()
this._$el.addClass('eruda-inline')
this.show()
}
},
_isInit: false,
version: VERSION,
util: {
isErudaEl: util.isErudaEl,
evalCss,
isDarkTheme(theme) {
if (!theme) {
theme = this.getTheme()
}
return isDarkTheme(theme)
},
getTheme: () => {
const curTheme = evalCss.getCurTheme()
let result = 'Light'
each(themes, (theme, name) => {
if (isEqual(theme, curTheme)) {
result = name
}
})
return result
},
},
chobitsu,
Tool,
Console,
Elements,
Network,
Sources,
Resources,
Info,
Snippets,
Settings,
get(name) {
if (!this._checkInit()) return
if (name === 'entryBtn') return this._entryBtn
const devTools = this._devTools
return name ? devTools.get(name) : devTools
},
add(tool) {
if (!this._checkInit()) return
if (isFn(tool)) tool = tool(this)
this._devTools.add(tool)
return this
},
remove(name) {
this._devTools.remove(name)
return this
},
show(name) {
if (!this._checkInit()) return
const devTools = this._devTools
name ? devTools.showTool(name) : devTools.show()
return this
},
hide() {
if (!this._checkInit()) return
this._devTools.hide()
return this
},
destroy() {
this._devTools.destroy()
delete this._devTools
this._entryBtn.destroy()
delete this._entryBtn
this._unregisterListener()
$(this._container).remove()
evalCss.clear()
this._isInit = false
this._container = null
this._shadowRoot = null
},
scale(s) {
if (isNum(s)) {
this._scale = s
emitter.emit(emitter.SCALE, s)
return this
}
return this._scale
},
position(p) {
const entryBtn = this._entryBtn
if (isObj(p)) {
entryBtn.setPos(p)
return this
}
return entryBtn.getPos()
},
_autoScale() {
if (!isMobile()) return
this.scale(1 / viewportScale())
},
_registerListener() {
this._addListener = (...args) => this.add(...args)
this._showListener = (...args) => this.show(...args)
emitter.on(emitter.ADD, this._addListener)
emitter.on(emitter.SHOW, this._showListener)
emitter.on(emitter.SCALE, evalCss.setScale)
},
_unregisterListener() {
emitter.off(emitter.ADD, this._addListener)
emitter.off(emitter.SHOW, this._showListener)
emitter.off(emitter.SCALE, evalCss.setScale)
},
_checkInit() {
if (!this._isInit) logger.error('Please call "eruda.init()" first')
return this._isInit
},
_initContainer(container, useShadowDom) {
if (!container) {
container = document.createElement('div')
document.documentElement.appendChild(container)
}
container.id = 'eruda'
container.style.all = 'initial'
this._container = container
let shadowRoot
let el
if (useShadowDom) {
if (container.attachShadow) {
shadowRoot = container.attachShadow({ mode: 'open' })
} else if (container.createShadowRoot) {
shadowRoot = container.createShadowRoot()
}
if (shadowRoot) {
// font-face doesn't work inside shadow dom.
evalCss.container = document.head
evalCss(
require('./style/icon.css') +
require('luna-console/luna-console.css') +
require('luna-object-viewer/luna-object-viewer.css') +
require('luna-dom-viewer/luna-dom-viewer.css') +
require('luna-text-viewer/luna-text-viewer.css') +
require('luna-notification/luna-notification.css')
)
el = document.createElement('div')
shadowRoot.appendChild(el)
this._shadowRoot = shadowRoot
}
}
if (!this._shadowRoot) {
el = document.createElement('div')
container.appendChild(el)
}
extend(el, {
className: 'eruda-container __chobitsu-hide__',
contentEditable: false,
})
// http://stackoverflow.com/questions/3885018/active-pseudo-class-doesnt-work-in-mobile-safari
if (detectBrowser().name === 'ios') el.setAttribute('ontouchstart', '')
this._$el = $(el)
},
_initDevTools(defaults, inline) {
this._devTools = new DevTools(this._$el, {
defaults,
inline,
})
},
_initStyle() {
const className = 'eruda-style-container'
const $el = this._$el
if (this._shadowRoot) {
evalCss.container = this._shadowRoot
evalCss(':host { all: initial }')
} else {
$el.append(`
`)
evalCss.container = $el.find(`.${className}`).get(0)
}
evalCss(
require('./style/reset.scss') +
require('luna-object-viewer/luna-object-viewer.css') +
require('luna-console/luna-console.css') +
require('luna-notification/luna-notification.css') +
require('luna-data-grid/luna-data-grid.css') +
require('luna-dom-viewer/luna-dom-viewer.css') +
require('luna-modal/luna-modal.css') +
require('luna-tab/luna-tab.css') +
require('luna-text-viewer/luna-text-viewer.css') +
require('luna-setting/luna-setting.css') +
require('luna-box-model/luna-box-model.css') +
require('./style/style.scss') +
require('./style/icon.css')
)
},
_initEntryBtn() {
this._entryBtn = new EntryBtn(this._$el)
this._entryBtn.on('click', () => this._devTools.toggle())
},
_initSettings() {
const devTools = this._devTools
const settings = new Settings()
devTools.add(settings)
this._entryBtn.initCfg(settings)
devTools.initCfg(settings)
},
_initTools(
tool = [
'console',
'elements',
'network',
'resources',
'sources',
'info',
'snippets',
]
) {
tool = toArr(tool)
const devTools = this._devTools
tool.forEach((name) => {
const Tool = this[upperFirst(name)]
try {
if (Tool) devTools.add(new Tool())
} catch (e) {
// Use nextTick to make sure it is possible to be caught by console panel.
nextTick(() => {
logger.error(
`Something wrong when initializing tool ${name}:`,
e.message
)
})
}
})
devTools.showTool(tool[0] || 'settings')
},
}
================================================
FILE: src/index.js
================================================
const eruda = require('./eruda').default
module.exports = eruda
module.exports.default = eruda
//# sourceMappingURL=index.js.map
================================================
FILE: src/lib/chobitsu.js
================================================
import Chobitsu from 'chobitsu/Chobitsu'
import * as Network from 'chobitsu/domains/Network'
import * as Overlay from 'chobitsu/domains/Overlay'
import * as DOM from 'chobitsu/domains/DOM'
import * as Storage from 'chobitsu/domains/Storage'
const chobitsu = new Chobitsu()
chobitsu.register('Network', Network)
chobitsu.register('Overlay', Overlay)
chobitsu.register('DOM', {
...DOM,
getNodeId: DOM.getDOMNodeId,
getNode: DOM.getDOMNode,
})
chobitsu.register('Storage', Storage)
export default chobitsu
================================================
FILE: src/lib/emitter.js
================================================
import Emitter from 'licia/Emitter'
const emitter = new Emitter()
emitter.ADD = 'ADD'
emitter.SHOW = 'SHOW'
emitter.SCALE = 'SCALE'
export default emitter
================================================
FILE: src/lib/empty.js
================================================
export default {}
================================================
FILE: src/lib/evalCss.js
================================================
import toStr from 'licia/toStr'
import each from 'licia/each'
import filter from 'licia/filter'
import isStr from 'licia/isStr'
import keys from 'licia/keys'
import kebabCase from 'licia/kebabCase'
import defaults from 'licia/defaults'
import themes from './themes'
let styleList = []
let scale = 1
let curTheme = themes.Light
const exports = function (css, container) {
css = toStr(css)
for (let i = 0, len = styleList.length; i < len; i++) {
if (styleList[i].css === css) return
}
container = container || exports.container || document.head
const el = document.createElement('style')
el.type = 'text/css'
container.appendChild(el)
const style = { css, el, container }
resetStyle(style)
styleList.push(style)
return style
}
exports.setScale = function (s) {
scale = s
resetStyles()
}
exports.setTheme = function (theme) {
if (isStr(theme)) {
curTheme = themes[theme] || themes.Light
} else {
curTheme = defaults(theme, themes.Light)
}
resetStyles()
}
exports.getCurTheme = () => curTheme
exports.getThemes = () => themes
exports.clear = function () {
each(styleList, ({ container, el }) => container.removeChild(el))
styleList = []
}
exports.remove = function (style) {
styleList = filter(styleList, (s) => s !== style)
style.container.removeChild(style.el)
}
function resetStyles() {
each(styleList, (style) => resetStyle(style))
}
function resetStyle({ css, el }) {
css = css.replace(/(\d+)px/g, ($0, $1) => +$1 * scale + 'px')
css = css.replace(/_/g, 'eruda-')
const _keys = keys(themes.Light)
each(_keys, (key) => {
css = css.replace(
new RegExp(`var\\(--${kebabCase(key)}\\)`, 'g'),
curTheme[key]
)
})
el.innerText = css
}
export default exports
================================================
FILE: src/lib/logger.js
================================================
import Logger from 'licia/Logger'
let logger
export default logger = new Logger(
'[Eruda]',
ENV === 'production' ? 'warn' : 'debug'
)
logger.formatter = function (type, argList) {
argList.unshift(this.name)
return argList
}
================================================
FILE: src/lib/micromark.js
================================================
export function micromark(str) {
return str
}
================================================
FILE: src/lib/themes.js
================================================
import extend from 'licia/extend'
import isArr from 'licia/isArr'
import contain from 'licia/contain'
const keyMap = [
'background',
'foreground',
'selectForeground',
'accent',
'highlight',
'border',
'primary',
'contrast',
'varColor',
'stringColor',
'keywordColor',
'numberColor',
'operatorColor',
'linkColor',
'textColor',
'tagNameColor',
'functionColor',
'attributeNameColor',
'commentColor',
]
const keyMapLen = keyMap.length
function arrToMap(arr) {
const ret = {}
for (let i = 0; i < keyMapLen; i++) {
ret[keyMap[i]] = arr[i]
}
return ret
}
function createDarkTheme(theme) {
if (isArr(theme)) theme = arrToMap(theme)
if (!theme.darkerBackground) theme.darkerBackground = theme.contrast
return extend(
{
consoleWarnBackground: '#332a00',
consoleWarnForeground: '#ffcb6b',
consoleWarnBorder: '#650',
consoleErrorBackground: '#290000',
consoleErrorForeground: '#ff8080',
consoleErrorBorder: '#5c0000',
light: '#ccc',
dark: '#aaa',
},
theme
)
}
function createLightTheme(theme) {
if (isArr(theme)) theme = arrToMap(theme)
if (!theme.darkerBackground) theme.darkerBackground = theme.contrast
return extend(
{
consoleWarnBackground: '#fffbe5',
consoleWarnForeground: '#5c5c00',
consoleWarnBorder: '#fff5c2',
consoleErrorBackground: '#fff0f0',
consoleErrorForeground: '#f00',
consoleErrorBorder: '#ffd6d6',
light: '#fff',
dark: '#eee',
},
theme
)
}
const darkThemes = [
'Dark',
'Material Oceanic',
'Material Darker',
'Material Palenight',
'Material Deep Ocean',
'Monokai Pro',
'Dracula',
'Arc Dark',
'Atom One Dark',
'Solarized Dark',
'Night Owl',
'AMOLED',
]
export function isDarkTheme(theme) {
return contain(darkThemes, theme)
}
// prettier-ignore
export default {
Light: createLightTheme({
darkerBackground: '#f3f3f3',
background: '#fff',
foreground: '#333',
selectForeground: '#333',
accent: '#1a73e8',
highlight: '#eaeaea',
border: '#ccc',
primary: '#333',
contrast: '#f2f7fd',
varColor: '#c80000',
stringColor: '#1a1aa6',
keywordColor: '#881280',
numberColor: '#1c00cf',
operatorColor: '#808080',
linkColor: '#1155cc',
textColor: '#8097bd',
tagNameColor: '#881280',
functionColor: '#222',
attributeNameColor: '#994500',
commentColor: '#236e25',
cssProperty: '#c80000',
}),
Dark: createDarkTheme({
darkerBackground: '#333',
background: '#242424',
foreground: '#a5a5a5',
selectForeground: '#eaeaea',
accent: '#7cacf8',
highlight: '#000',
border: '#3d3d3d',
primary: '#ccc',
contrast: '#0b2544',
varColor: '#e36eec',
stringColor: '#f29766',
keywordColor: '#9980ff',
numberColor: '#9980ff',
operatorColor: '#7f7f7f',
linkColor: '#ababab',
textColor: '#42597f',
tagNameColor: '#5db0d7',
functionColor: '#d5d5d5',
attributeNameColor: '#9bbbdc',
commentColor: '#747474',
}),
'Material Oceanic': createDarkTheme([
'#263238', '#B0BEC5', '#FFFFFF', '#009688', '#425B67',
'#2A373E', '#607D8B', '#1E272C', '#eeffff', '#c3e88d',
'#c792ea', '#f78c6c', '#89ddff', '#80cbc4', '#B0BEC5',
'#f07178', '#82aaff', '#ffcb6b', '#546e7a',
]),
'Material Darker': createDarkTheme([
'#212121', '#B0BEC5', '#FFFFFF', '#FF9800', '#3F3F3F',
'#292929', '#727272', '#1A1A1A', '#eeffff', '#c3e88d',
'#c792ea', '#f78c6c', '#89ddff', '#80cbc4', '#B0BEC5',
'#f07178', '#82aaff', '#ffcb6b', '#616161',
]),
'Material Lighter': createLightTheme([
'#FAFAFA', '#546E7A', '#546e7a', '#00BCD4', '#E7E7E8',
'#d3e1e8', '#94A7B0', '#F4F4F4', '#272727', '#91B859',
'#7C4DFF', '#F76D47', '#39ADB5', '#39ADB5', '#546E7A',
'#E53935', '#6182B8', '#F6A434', '#AABFC9',
]),
'Material Palenight': createDarkTheme([
'#292D3E', '#A6ACCD', '#FFFFFF', '#ab47bc', '#444267',
'#2b2a3e', '#676E95', '#202331', '#eeffff', '#c3e88d',
'#c792ea', '#f78c6c', '#89ddff', '#80cbc4', '#A6ACCD',
'#f07178', '#82aaff', '#ffcb6b', '#676E95',
]),
'Material Deep Ocean': createDarkTheme([
'#0F111A', '#8F93A2', '#FFFFFF', '#84ffff', '#1F2233',
'#41465b', '#4B526D', '#090B10', '#eeffff', '#c3e88d',
'#c792ea', '#f78c6c', '#89ddff', '#80cbc4', '#8F93A2',
'#f07178', '#82aaff', '#ffcb6b', '#717CB4',
]),
'Monokai Pro': createDarkTheme([
'#2D2A2E', '#fcfcfa', '#FFFFFF', '#ffd866', '#5b595c',
'#423f43', '#939293', '#221F22', '#FCFCFA', '#FFD866',
'#FF6188', '#AB9DF2', '#FF6188', '#78DCE8', '#fcfcfa',
'#FF6188', '#A9DC76', '#78DCE8', '#727072',
]),
Dracula: createDarkTheme([
'#282A36', '#F8F8F2', '#8BE9FD', '#FF79C5', '#6272A4',
'#21222C', '#6272A4', '#191A21', '#F8F8F2', '#F1FA8C',
'#FF79C6', '#BD93F9', '#FF79C6', '#F1FA8C', '#F8F8F2',
'#FF79C6', '#50FA78', '#50FA7B', '#6272A4',
]),
'Arc Dark': createDarkTheme([
'#2f343f', '#D3DAE3', '#FFFFFF', '#42A5F5', '#3F3F46',
'#404552', '#8b9eb5', '#262b33', '#CF6A4C', '#8F9D6A',
'#9B859D', '#CDA869', '#A7A7A7', '#7587A6', '#D3DAE3',
'#CF6A4C', '#7587A6', '#F9EE98', '#747C84',
]),
'Atom One Dark': createDarkTheme([
'#282C34', '#979FAD', '#FFFFFF', '#2979ff', '#383D48',
'#2e3239', '#979FAD', '#21252B', '#D19A66', '#98C379',
'#C679DD', '#D19A66', '#61AFEF', '#56B6C2', '#979FAD',
'#F07178', '#61AEEF', '#E5C17C', '#59626F',
]),
'Atom One Light': createLightTheme([
'#FAFAFA', '#232324', '#232324', '#2979ff', '#EAEAEB',
'#DBDBDC', '#9D9D9F', '#FFFFFF', '#986801', '#50A14E',
'#A626A4', '#986801', '#4078F2', '#0184BC', '#232324',
'#E4564A', '#4078F2', '#C18401', '#A0A1A7',
]),
'Solarized Dark': createDarkTheme([
'#002B36', '#839496', '#FFFFFF', '#d33682', '#11353F',
'#0D3640', '#586e75', '#00252E', '#268BD2', '#2AA198',
'#859900', '#D33682', '#93A1A1', '#268BD2', '#839496',
'#268BD2', '#B58900', '#B58900', '#657B83',
]),
'Solarized Light': createLightTheme([
'#fdf6e3', '#586e75', '#002b36', '#d33682', '#F6F0DE',
'#f7f2e2', '#93a1a1', '#eee8d5', '#268BD2', '#2AA198',
'#859900', '#D33682', '#657B83', '#268BD2', '#586e75',
'#268BD2', '#B58900', '#657B83', '#93A1A1',
]),
Github: createLightTheme([
'#F7F8FA', '#5B6168', '#FFFFFF', '#79CB60', '#CCE5FF',
'#DFE1E4', '#292D31', '#FFFFFF', '#24292E', '#032F62',
'#D73A49', '#005CC5', '#D73A49', '#005CC5', '#5B6168',
'#22863A', '#6F42C1', '#6F42C1', '#6A737D',
]),
'Night Owl': createDarkTheme([
'#011627', '#b0bec5', '#ffffff', '#7e57c2', '#152C3B',
'#2a373e', '#607d8b', '#001424', '#addb67', '#ecc48d',
'#c792ea', '#f78c6c', '#c792ea', '#80CBC4', '#b0bec5',
'#7fdbca', '#82AAFF', '#FAD430', '#637777',
]),
'Light Owl': createLightTheme([
'#FAFAFA', '#546e7a', '#403f53', '#269386', '#E0E7EA',
'#efefef', '#403F53', '#FAFAFA', '#0C969B', '#c96765',
'#994cc3', '#aa0982', '#7d818b', '#994cc3', '#546e7a',
'#994cc3', '#4876d6', '#4876d6', '#637777',
]),
AMOLED: createDarkTheme([
'#000000', '#8F93A2', '#FFFFFF', '#68FFAE', '#000000',
'#41465b', '#4B526D', '#000000', '#DEFDF7', '#38ff9f',
'#ab2eff', '#A76DF7', '#38ff9f', '#86F3C7', '#8F93A2',
'#ab2eff', '#8293FF', '#38ff9f', '#6575c7',
]),
}
================================================
FILE: src/lib/util.js
================================================
import Url from 'licia/Url'
import contain from 'licia/contain'
import escapeJsStr from 'licia/escapeJsStr'
import isUndef from 'licia/isUndef'
import last from 'licia/last'
import map from 'licia/map'
import memStorage from 'licia/memStorage'
import toNum from 'licia/toNum'
import trim from 'licia/trim'
import html from 'licia/html'
// https://stackoverflow.com/questions/46318395/detecting-mobile-device-notch
export function hasSafeArea() {
let proceed = false
const div = document.createElement('div')
if (CSS.supports('padding-bottom: env(safe-area-inset-bottom)')) {
div.style.paddingBottom = 'env(safe-area-inset-bottom)'
proceed = true
} else if (CSS.supports('padding-bottom: constant(safe-area-inset-bottom)')) {
div.style.paddingBottom = 'constant(safe-area-inset-bottom)'
proceed = true
}
if (proceed) {
document.body.appendChild(div)
const calculatedPadding = parseInt(
window.getComputedStyle(div).paddingBottom
)
document.body.removeChild(div)
if (calculatedPadding > 0) {
return true
}
}
return false
}
export function escapeJsonStr(str) {
return escapeJsStr(str).replace(/\\'/g, "'").replace(/\t/g, '\\t')
}
export function safeStorage(type, memReplacement) {
if (isUndef(memReplacement)) memReplacement = true
let ret
switch (type) {
case 'local':
ret = window.localStorage
break
case 'session':
ret = window.sessionStorage
break
}
try {
// Safari private browsing
const x = 'test-localStorage-' + Date.now()
ret.setItem(x, x)
const y = ret.getItem(x)
ret.removeItem(x)
if (y !== x) throw new Error()
} catch {
if (memReplacement) return memStorage
return
}
return ret
}
export function getFileName(url) {
let ret = last(url.split('/'))
if (ret === '') {
url = new Url(url)
ret = url.hostname
}
return ret
}
export function pxToNum(str) {
return toNum(str.replace('px', ''))
}
export function isErudaEl(el) {
while (el) {
if (el.id === 'eruda') return true
el = el.parentNode
}
return false
}
export function isChobitsuEl(el) {
while (el) {
let className = ''
if (el.getAttribute) {
className = el.getAttribute('class') || ''
}
if (contain(className, '__chobitsu-hide__')) {
return true
}
el = el.parentNode
}
return false
}
export function classPrefix(str) {
if (/<[^>]*>/g.test(str)) {
try {
const tree = html.parse(str)
traverseTree(tree, (node) => {
if (node.attrs && node.attrs.class) {
node.attrs.class = processClass(node.attrs.class)
}
})
return html.stringify(tree)
} catch {
return processClass(str)
}
}
return processClass(str)
}
function traverseTree(tree, handler) {
for (let i = 0, len = tree.length; i < len; i++) {
const node = tree[i]
handler(node)
if (node.content) {
traverseTree(node.content, handler)
}
}
}
function processClass(str) {
const prefix = 'eruda-'
return map(trim(str).split(/\s+/), (singleClass) => {
if (contain(singleClass, prefix)) {
return singleClass
}
return singleClass.replace(/[\w-]+/, (match) => `${prefix}${match}`)
}).join(' ')
}
export function eventClient(type, e) {
const name = type === 'x' ? 'clientX' : 'clientY'
if (e[name]) {
return e[name]
}
if (e.changedTouches) {
return e.changedTouches[0][name]
}
return 0
}
export function eventPage(type, e) {
const name = type === 'x' ? 'pageX' : 'pageY'
if (e[name]) {
return e[name]
}
if (e.changedTouches) {
return e.changedTouches[0][name]
}
return 0
}
================================================
FILE: src/polyfill.js
================================================
import 'core-js/modules/es.map'
import 'core-js/stable/promise'
================================================
FILE: src/style/icon.css
================================================
@font-face {
font-family: 'eruda-icon';
src: url('data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAA7UAAsAAAAAGoAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAARAAAAHGLTYxKE9TLzIAAAIYAAAAPwAAAFZWm1KoY21hcAAAAlgAAAFTAAADwhIFxPxnbHlmAAADrAAACEoAAA78hUhXQGhlYWQAAAv4AAAAMQAAADZ26MSyaGhlYQAADCwAAAAdAAAAJAgEBC9obXR4AAAMTAAAAB0AAACwXAv//GxvY2EAAAxsAAAAOwAAAFpp4mZEbWF4cAAADKgAAAAfAAAAIAE9AQ1uYW1lAAAMyAAAASkAAAIWm5e+CnBvc3QAAA30AAAA3wAAAUT1LH8yeJxNkLtOw0AQRe+GmJhXeFlhnRgwsTF2aChSUVAgRJUKUdBGioSQohQRX8YXcnZMAK/GOztzZubuykna1Z0e1Hl6nr2qv5x/rjRUV+0X8v99t/x4nyvenMh1bY/l3IJOQ9V60UJrfbnEVW7qHo0KlpIdaQt2oLH2ta1b6CtVKqiMlLFK7ABmAh1q+lDn8tSlZEJkgJUsz97Om+pYPeue8W90qSOqUuXWraBbRI8L4iOd4AcmxY/QUOpeM73h76Eks/iYfcOHGX/xAv4avT3uUOqG2e3EhnkRilJjY1NYc4egd8e0eOvQ3ukMC1xlp0PMc56oA5no1PiQz1GQ/Fbn1DvU+J93DPkCNbWab0arGKZ4nGNgZJJgnMDAysDA1Mt0hoGBoR9CM75mMGLkAIoysDIzYAUBaa4pDAcYdD+KsIC4MSxMDIxAGoQZALgnCOUAeJy100lOw0AQheHfZGAmAYLIPM+BQ0VhlJgUIkFuw4FYcRLqBPDaXQu2IFHRl8hlt7vjfgZyQEbOJAvJBwmh3tVN0n6GnbSf5U3HRX3C9efMueWJFWsrfX15Z8EdS14sSTs/K0lHFqnTZ5zO1qVDmx4VqjRp0aDMiGF6vs6EGgNmTDVyQ3PnyLPJFttazS77HFDQ3Q454pgSJ5xqijy/q+4vrw+lNfcq1WarAeXRsK+lTmowmE3/cK//qL3wlXz60ZiwL1H4t3PXkYVry4XryaWryJWryrVryo1rya3TE9HuR2W5dyN5cEN5dH15cmGdS1eXZzeRldNj5sUN5NXNZO20C5ZEIb22ERF+M1FIubmQfnPKF+aUNMwpc5hT+jAX3gpzSiTmlE1sL1JKsf1IecUOIiUXK0TKMFaMlGbsMFKusaNICceOI2UdK0WcfgOIClGwAHic7VddbBTXFb7n3vnZn9mZnd3Zmd21Peud9c54jb1e7y+sje2NYyg11OC/8mOww58KVCEkokACCn9CeYioitL2ASmVUNKH5IEkPFhFDRKoVZW2SquEqqr61odUlUqrChX1JzvuvePFpq3y0Lc+4LHOnHvPued+e86559xBgOgf/oBsQTxC2ZofHMAnN7oPIbTRXVoaghCEhtwlhP5Nzw+iHwz8gbu0EULuw40YwZYh96H7cOh/0yOLhEeYGk8DWWgegEnCud98vNdPyaQnUwF2wgky2dwPk3SJt+4t8jaKog6UpXJblLGJa1X22A59RJOIMAwGDzLkcRW3d6Ze/OF+7dIvThzb9dWBEq6Udu6+svXiNj7jbnfmB9y/wfvV868ermByA2Pfqx//7va+YGCoPr/w6327N9RP4okL3xmDr5smvOm+0R1Pz56+xOB5ON4j7yEDFRhKTbD6QWTELo+AY5erpXRRB00BQ9eETNqyK2q5WkxBjREdI70jOP2hoAgNnr8zFdRlQIoON1f4ZcqvyeG3y0jWg1N3eL5BJz6cDnboOpwQW5wCa9KWf14gR1ES9aA8RbYCJEYBrgOLwhoENR2jaO1KZk3k4YuqaRUakRD8JRSJhG4zct3dFklGbrM5KRqVXCUUGYZbZILJ2IAK4dZ1NnospqqfuNvgFmrFkYyRMRSkA9EQjZpRc2qOCNG5B3+cffDAo2RsrsVRivDyMkIccIiiH6OrBAUEJ0/9OQL9kIeqTn3HfKgbuoINXZSpz6nL+yGrGSZ1dbE2TKVlb4nlSeFROD0Y7whFMKedtKZU3JlTj2COnNE3CFzsaPCUEeB0OaEdwxxuut3O1fmFXy7MX3W6n2DhJrXR0vtGekrtSWH1KOb500bUTGrHfKeNDpnZP4YFbvxr/7naY1dzZoksobiXM1+UG1+YTH8XaHbEFC87zMBKdghwZ41fkXuZ9LaPpkfA9NJDidH0EJjuZ8trA6Wl8H+LayWXf0XmUASl0ABF5kV0NcwsxsTKQ3kYiiZoMhBLxjHNhJI+jMt5DP88srj3ajZ7de/ip4+Zo/UD9fqBk4wMJgpdWmYwk6QvratAZnO5a/sWrnX39HRfW9h3LZdrftbSpAR2UZVE16AF9F2iK9DjWvQjchkFqOfaaf2kuCpQSccgZrBSpHq8SoGV1Ax8z3kxB7hZB7wwkitlXZe85rq9B184e/bPitoER5Ld37gneEnpBD90f/u7dPTXjk645H6+epYa5DxSWF21M5ZAz61eKtagWiY9zfvBoGoYakDCvTrOw1bBUJvbVUOEich6tLr+MnkJxRCKaobOfEgrZsUrmZYoiMI5Vhg/2b2zVOGq5bldP985V6lgdIRNMMmRw3MzA8XiwMzck/auoC52tvOQ8Ww8YThDA7ISnFJRN8QsOXSocnnzqjlvowvjp14/NT5+alN+2joJNXLlYH9xVehtuomJqU6q/aWVtuDte4G8iyTUoDsXh6GcB0sGGvZsHjt2piIalSKrBSbENK8CiCsVhCKi4Oh0iQGk+TMMlTJGmUHLGtzECFxXZs5N2N3vvJMy68+lE8l4sDM2snNoiA5Ks1Znz8bC8xsce2LErGjpNMxm6t6yTfXMx18+NxMW586fnxWkxkB2tm9ofVtBG4lIktQobN2z4fn+4Z5Oy6esi5VT/dTG499xk3yEErTSIcg4DE0e0wzWTFwqVmtGzKhlrTwuD+OiiTUZ4/e7ZSdXmn/54pm9xeLe088MHu8LuRPNT/tmRjOZ0Zk9M6OWNToD10N9xwefOc10zlx8eb6Uc+Tu5tam1Zje01KcblhrvvwxuUR7aZJmb8aix50eaBbDDshU0mqJVW1HJHD+xuIiHl+XiIhco/nKPfzs3buj9y5cOPscHl+MR9qV4I17zVcAX7g3evcupsUbiQgtL3M8B+hb6F10Hz1C/0BNFkGojkCF5lyeBsbLvVbXFlicrJZAoa2bFhxa31nwmEyBGAue98RogfKm2KwXUa8fMG1KUlDy7gIlo7gy8oJNKxbbqlZlUzp9UxSt7aH1XpEarV1F2i5rtoeHraWpRT3hbeZZYBw2HG+JQ3EwruaBsr0O5DALVIttRI3TvkdXO6JBfiKbaiQeFPVIRz7R6ZfC2C9WxqQkp8Rj2/WkQtpEs56SAxy9xBA+3a/06m3+gB7mOFGN+kKcyAH4eEkMxORwUhIN1exLdPokmUhie70rETC0VDSnCTLvC0vxXLlN9QsgYGqpENCCBlGjopQI94ajhIicO3pwarK3j5Biccf0woGp7X15jAuFyakF6fd8INTev+VZOeLzx7JWsb8Bok+qxdV4T39cpXO2VdJ9Ulj6SOvm/CHJxymKXw1xAtEJL+aKRpTT8k6iP0v4zbE3e2e2fukQTuGvBNRA3Km2qQFRkhKWnsryBm6Lc0FfpyQFQ2kxQPyJkBBVAnow4hvoDIZ9sp4sd4cB8/6gEAjoZiTrE3ycjxckMWwPpVYspeNmVtA5ORm2owqPISCE2mNRg3mZ04NRfyElECwC7w+3V+14MhxtM6qKu4uUBmZmD+/bvq03T3BxYMf0/oUdk/nCD2wtIUVHNivReIcpk3p5vdCjJiU1mMgVEhE2hzeYgt8nDFKH+sIRRSUiHwpKQcEQpZBdiGs9Wr4rPuAQ4S3ttdwfJiaPQ+rpmXh6Jp6eif86E2S5Se/7hMNIpj0oQ7sQveULIgi0Tadpc6efLytfKPQuarDPlyK9mZI/ufNatapR6q+uI4V1VX/zZ2aPSf9LbdlsNZt9g/TabW325/edhm038O6Yacbc7zP6KFux7Qr9cET/AqBddaEAAHicY2BkYGAA4irvctZ4fpuvDNwsIIEozsf7GmD0/7///7OwsjABJTgYQCQDADqDDA8AAAB4nGNgZGBgYQABFtb/f///ZWFlYGRABToAW+YEPQAAAHicY2BgYGAhCv//T5w6IGaFYob/fxmoDADd6QRhAAAAeJxjYAACEQYNBhsGL4YIhjkMLxj1GD0YtzExMfkxzWO6wvSFWYY5gnkP8z8WHdZt7A7sdcRCAEgDFOMAeJxjYGRgYNBhZGRgZwABJiDmAkIGhv9gPgMADcIBTAB4nGWQPW7CQBSEx2BIAlKCFCkps1UKIpmfkgNAT0GXwpi1MbK91npBossJcoQcIaeIcoIcKGPzaGAtP38zb97uygAG+IWHenm4bWq9WrihOnGb9CDsk5+FO+jjRbhLfyjcwxumwn084p07eP4dnQFK4Rbu8SHcpv8p7JO/hDt4wrdwl/6PcA8r/An38eoN08gUsSncUif7LLRnef6utK1SU6hJMD5bC11oGzq9Ueujqg7J1LlYxdbkas6uzjKjSmt2OnLB1rlyNhrF4geRyZEigkGBuKkOS2gk2CNDCHvVvdQrpi0q+rVWmCDA+Cq1YKpokiGVxobJNY6sFQ48bUrXMa34Ws7kpLnMat4kIyv+77q3oxPRD7BtpkrMMOITX+SD5g75Pz0RXqgAAAB4nG2M2Y6CQBREOQqto7Pv+76PPPhJpLmISUuTSyfK3w8jr3MeTlVSSUWDqGcS/c+MAUNiEgwjxuwwYcoue+xzwCFHHHPCKWecc8ElV1xzwy133PPAI08888Irb7zzwSdffPPDjDSKnRQh0eWiDKOt0/nEZiohzf26mvZ1OyTWSaZj61e1StPE1tetycVJkERUvRrZ1FmVj/tI50NpxRRLF0Tj2mWtUbFe85FK0R2USScJpulObdmFExvi4L0zf0rn8TrTCodQEFCWLCixZKTkeNZUrKhpaNlE0S9fBESeAA==')
format('woff');
}
[class^='icon-'],
[class*=' icon-'] {
display: inline-block;
font-family: 'eruda-icon' !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-left:before {
content: '\f101';
}
.icon-right:before {
content: '\f102';
}
.icon-caret-down:before {
content: '\f103';
}
.icon-caret-right:before {
content: '\f104';
}
.icon-clear:before {
content: '\f105';
}
.icon-compress:before {
content: '\f106';
}
.icon-copy:before {
content: '\f107';
}
.icon-delete:before {
content: '\f108';
}
.icon-error:before {
content: '\f109';
}
.icon-expand:before {
content: '\f10a';
}
.icon-eye:before {
content: '\f10b';
}
.icon-filter:before {
content: '\f10c';
}
.icon-play:before {
content: '\f10d';
}
.icon-record:before {
content: '\f10e';
}
.icon-refresh:before {
content: '\f10f';
}
.icon-reset:before {
content: '\f110';
}
.icon-search:before {
content: '\f111';
}
.icon-select:before {
content: '\f112';
}
.icon-tool:before {
content: '\f113';
}
.icon-warn:before {
content: '\f114';
}
================================================
FILE: src/style/icon.json
================================================
[
"left.svg",
"right.svg",
"caret-down.svg",
"caret-right.svg",
"clear.svg",
"compress.svg",
"copy.svg",
"delete.svg",
"error.svg",
"expand.svg",
"eye.svg",
"filter.svg",
"play.svg",
"record.svg",
"refresh.svg",
"reset.svg",
"search.svg",
"select.svg",
"tool.svg",
"warn.svg"
]
================================================
FILE: src/style/luna.scss
================================================
@use './variable' as *;
.container {
.luna-console {
background: var(--background);
}
@mixin luna-console-highlight {
.luna-console-key {
color: var(--var-color);
}
.luna-console-number {
color: var(--number-color);
}
.luna-console-null {
color: var(--operator-color);
}
.luna-console-string {
color: var(--string-color);
}
.luna-console-boolean {
color: var(--keyword-color);
}
.luna-console-special {
color: var(--operator-color);
}
.luna-console-keyword {
color: var(--keyword-color);
}
.luna-console-operator {
color: var(--operator-color);
}
.luna-console-comment {
color: var(--comment-color);
}
}
.luna-console-header {
color: var(--link-color);
border-bottom-color: var(--border);
}
.luna-console-nesting-level {
border-right-color: var(--border);
&::before {
border-bottom-color: var(--border);
}
}
.luna-console-log-container {
&.luna-console-selected {
.luna-console-log-item {
background: var(--contrast);
&:not(.luna-console-error):not(.luna-console-warn) {
border-color: var(--border);
}
}
}
}
.luna-console-log-item {
border-bottom-color: var(--border);
color: var(--foreground);
a {
color: var(--link-color) !important;
}
.luna-console-icon-container {
.luna-console-icon {
color: var(--foreground);
}
.luna-console-icon-error {
color: #ef3842;
}
.luna-console-icon-warn {
color: #e8a400;
}
}
.luna-console-count {
color: var(--select-foreground);
background: var(--highlight);
}
&.luna-console-warn {
color: var(--console-warn-foreground);
background: var(--console-warn-background);
border-color: var(--console-warn-border);
}
&.luna-console-error {
background: var(--console-error-background);
color: var(--console-error-foreground);
border-color: var(--console-error-border);
.luna-console-count {
background: var(--console-error-foreground);
}
}
.luna-console-code {
@include luna-console-highlight();
}
.luna-console-log-content {
.luna-console-undefined,
.luna-console-null {
color: var(--operator-color);
}
.luna-console-number {
color: var(--number-color);
}
.luna-console-boolean {
color: var(--keyword-color);
}
.luna-console-symbol,
.luna-console-regexp {
color: var(--var-color);
}
}
}
.luna-console-preview {
@include luna-console-highlight();
}
.luna-object-viewer {
color: var(--primary);
font-size: 12px !important;
}
.luna-object-viewer-null {
color: var(--operator-color);
}
.luna-object-viewer-string,
.luna-object-viewer-regexp {
color: var(--string-color);
}
.luna-object-viewer-number {
color: var(--number-color);
}
.luna-object-viewer-boolean {
color: var(--keyword-color);
}
.luna-object-viewer-special {
color: var(--operator-color);
}
.luna-object-viewer-key,
.luna-object-viewer-key-lighter {
color: var(--var-color);
}
.luna-object-viewer-expanded:before {
border-color: transparent;
border-top-color: var(--foreground);
}
.luna-object-viewer-collapsed:before {
border-top-color: transparent;
border-left-color: var(--foreground);
}
.luna-notification {
pointer-events: none !important;
padding: $padding;
z-index: 1000;
}
.luna-notification-item {
z-index: 500;
color: var(--foreground);
background: var(--background);
box-shadow: none;
padding: 5px 10px;
border: 1px solid var(--border);
}
.luna-notification-upper {
margin-bottom: 10px;
}
.luna-notification-lower {
margin-top: 10px;
}
.luna-data-grid {
color: var(--foreground);
background: var(--background);
border-color: var(--border);
th,
td {
border-color: var(--border);
}
th {
background: var(--darker-background);
&.luna-data-grid-sortable {
&:hover,
&:active {
color: var(--select-foreground);
background: var(--highlight);
}
}
}
.luna-data-grid-data-container {
.luna-data-grid-node.luna-data-grid-selected,
.luna-data-grid-node.luna-data-grid-selectable:hover {
background: var(--highlight);
}
tr:nth-child(even) {
background: var(--contrast);
}
}
&:focus {
.luna-data-grid-data-container {
.luna-data-grid-node.luna-data-grid-selected {
background: var(--accent);
}
}
}
}
.luna-dom-viewer {
color: var(--foreground);
.luna-dom-viewer-html-tag,
.luna-dom-viewer-tag-name {
color: var(--tag-name-color);
}
.luna-dom-viewer-attribute-name {
color: var(--attribute-name-color);
}
.luna-dom-viewer-attribute-value {
color: var(--string-color);
}
.luna-dom-viewer-html-comment {
color: var(--comment-color);
}
.luna-dom-viewer-tree-item {
&:hover {
.luna-dom-viewer-selection {
background: var(--contrast);
}
}
&.luna-dom-viewer-selected {
.luna-dom-viewer-selection {
background: var(--highlight);
}
}
&.luna-dom-viewer-selected:focus {
.luna-dom-viewer-selection {
background: var(--accent);
opacity: 0.2;
}
}
}
.luna-dom-viewer-text-node {
.luna-dom-viewer-key {
color: var(--var-color);
}
.luna-dom-viewer-number {
color: var(--number-color);
}
.luna-dom-viewer-null {
color: var(--operator-color);
}
.luna-dom-viewer-string {
color: var(--string-color);
}
.luna-dom-viewer-boolean {
color: var(--keyword-color);
}
.luna-dom-viewer-special {
color: var(--operator-color);
}
.luna-dom-viewer-keyword {
color: var(--keyword-color);
}
.luna-dom-viewer-operator {
color: var(--operator-color);
}
.luna-dom-viewer-comment {
color: var(--comment-color);
}
}
}
.luna-dom-viewer-children {
margin: 0;
padding-left: 15px !important;
}
.inline {
.luna-modal,
.luna-notification {
position: absolute;
}
}
.luna-modal {
z-index: 9999999;
}
.luna-modal-body,
.luna-modal-input {
color: var(--foreground);
background: var(--background);
}
.luna-modal-body {
border-color: var(--border);
}
.luna-modal-input {
user-select: text !important;
border-color: var(--border);
}
.luna-modal-button-group {
.luna-modal-secondary {
border-color: var(--border);
color: var(--foreground);
background: var(--background);
}
.luna-modal-primary {
background: var(--accent);
}
.luna-modal-button {
&:active {
&::before {
background: var(--accent);
}
}
}
}
.luna-tab {
position: absolute;
left: 0;
top: 0;
color: var(--foreground);
background: var(--darker-background);
}
.luna-tab-tabs-container {
border-color: var(--border);
}
.luna-tab-item {
&.luna-tab-selected,
&:hover {
background: var(--highlight);
color: var(--select-foreground);
}
}
.luna-tab-slider {
background: var(--accent);
}
.luna-text-viewer {
color: var(--foreground);
border: none;
border-bottom: 1px solid var(--border);
background: var(--background);
font-size: $font-size-s;
.luna-text-viewer-line-text {
user-select: text;
* {
user-select: text;
}
}
.luna-text-viewer-copy,
.luna-text-viewer-line-number {
border-color: var(--border);
}
.luna-text-viewer-copy .luna-text-viewer-icon-check {
color: var(--accent);
}
.luna-text-viewer-copy {
background-color: var(--background);
}
}
.luna-setting {
color: var(--foreground);
background: var(--background);
}
.luna-setting-item {
&:hover,
&.luna-setting-selected {
background: var(--darker-background);
}
&.luna-setting-selected:focus {
outline: none;
}
}
.luna-setting-item-title {
font-size: $font-size;
}
.luna-setting-item-separator {
border-color: var(--border);
}
.luna-setting-item-checkbox {
input {
border-color: var(--border);
&:checked {
background-color: var(--accent);
border-color: var(--accent);
}
}
}
.luna-setting-item-select {
.luna-setting-select {
select {
color: var(--foreground);
border-color: var(--border);
background: var(--background);
}
&:after {
border-top-color: var(--foreground);
}
}
}
.luna-setting-item-button {
button {
color: var(--accent);
background: var(--background);
border-color: var(--border);
&:hover,
&:active {
background: var(--darker-background);
}
&:active {
border: 1px solid var(--accent);
}
}
}
.luna-setting-item-number {
.luna-setting-range-container {
.luna-setting-range-track {
.luna-setting-range-track-bar {
background: var(--border);
.luna-setting-range-track-progress {
background: var(--accent);
}
}
}
input::-webkit-slider-thumb {
border-color: var(--border);
background: radial-gradient(
circle at center,
var(--dark) 0,
var(--dark) 15%,
var(--light) 22%,
var(--light) 100%
);
}
}
}
.luna-box-model {
background: var(--background);
}
.luna-box-model-position,
.luna-box-model-margin,
.luna-box-model-border,
.luna-box-model-padding,
.luna-box-model-content {
color: var(--foreground);
background: var(--background);
}
}
================================================
FILE: src/style/mixin.scss
================================================
@use './variable' as *;
@mixin absolute($width: 100%, $height: 100%) {
position: absolute;
width: $width;
height: $height;
left: 0;
top: 0;
}
@mixin overflow-auto($direction: 'both') {
@if $direction == 'both' {
overflow: auto;
} @else {
overflow-#{$direction}: auto;
}
-webkit-overflow-scrolling: touch;
}
@mixin safe-area($prop, $value, $pos: 'bottom') {
#{$prop}: calc(#{$value} + env(safe-area-inset-#{$pos}));
}
@mixin breadcrumb {
background: var(--darker-background);
color: var(--primary);
user-select: text;
margin-bottom: 10px;
word-break: break-all;
padding: $padding;
font-size: $font-size-l;
min-height: 40px;
border-bottom: 1px solid var(--border);
}
@mixin control {
@include absolute(100%, 40px);
cursor: default;
font-size: 0;
background: var(--darker-background);
color: var(--primary);
line-height: 20px;
border-bottom: 1px solid var(--border);
[class^='eruda-icon-'],
[class*=' icon-'] {
display: inline-block;
padding: 10px;
font-size: $font-size-l;
position: absolute;
top: 0;
cursor: pointer;
transition: color $anim-duration;
&:active,
&.active {
color: var(--accent);
}
}
}
@mixin clear-float {
&:after {
content: '';
display: block;
clear: both;
}
}
@mixin right-btn {
.btn {
margin-left: 5px;
float: right;
color: var(--primary);
width: 18px;
height: 18px;
font-size: $font-size-l;
font-weight: normal;
cursor: pointer;
transition: color $anim-duration;
&.filter-text {
width: auto;
max-width: 80px;
font-size: $font-size;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
}
&:active {
color: var(--accent);
}
&.btn-disabled {
color: inherit !important;
cursor: default !important;
pointer-events: none;
opacity: 0.5;
* {
pointer-events: none;
}
}
}
}
================================================
FILE: src/style/reset.scss
================================================
.container {
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
line-height: 1;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
color: inherit;
font-size: 1em;
font-style: inherit;
font-variant: inherit;
font-weight: inherit;
line-height: inherit;
text-decoration: inherit;
white-space: inherit;
}
}
================================================
FILE: src/style/style.scss
================================================
@use 'variable' as *;
@use 'mixin' as *;
@use 'luna' as *;
.container {
min-width: 320px;
pointer-events: none;
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 9999999;
color: var(--foreground);
font-family: $font-family;
font-size: $font-size;
direction: ltr;
&.dark {
color-scheme: dark;
}
* {
box-sizing: border-box;
pointer-events: all;
user-select: none;
-webkit-tap-highlight-color: transparent;
-webkit-text-size-adjust: none;
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
h1,
h2,
h3,
h4 {
margin: 0;
}
h2 {
font-size: $font-size;
[class^='icon-'],
[class*=' icon-'] {
font-weight: normal;
}
}
&.inline {
position: static;
}
}
.hidden {
display: none;
}
.icon-disabled {
opacity: 0.5;
pointer-events: none;
cursor: default !important;
&:active {
color: inherit !important;
}
}
.tag-name-color {
color: var(--tag-name-color);
}
.function-color {
color: var(--function-color);
}
.attribute-name-color {
color: var(--attribute-name-color);
}
.operator-color {
color: var(--operator-color);
}
.string-color {
color: var(--string-color);
}
================================================
FILE: src/style/variable.scss
================================================
$padding: 10px;
$font-size: 14px;
$font-size-s: 12px;
$font-size-l: 16px;
$font-family: -apple-system, system-ui, BlinkMacSystemFont,
'.SFNSDisplay-Regular', 'Helvetica Neue', 'Lucida Grande', 'Segoe UI', Tahoma,
sans-serif;
$anim-duration: 0.3s;
================================================
FILE: test/boot.js
================================================
function boot(name, cb) {
// Need a little delay to make sure width and height of webpack dev server iframe are initialized.
setTimeout(function () {
let options = {
useShadowDom: false,
defaults: {
displaySize: 50,
transparency: 0.9,
theme: 'Monokai Pro',
},
}
if (name) {
options.tool = name === 'settings' ? [] : name
}
try {
eruda.init(options)
} catch (e) {
alert(e)
}
eruda.show()
cb && cb()
if (name == null) return
loadJs('lib/boot', function () {
loadJs('lib/jasmine-jquery', function () {
// This is needed to trigger jasmine initialization.
loadJs(name, function () {
window.onload()
})
})
})
}, 500)
}
function loadJs(src, cb) {
let script = document.createElement('script')
script.src = src + '.js'
script.onload = cb
document.body.appendChild(script)
}
================================================
FILE: test/console.html
================================================
Console
================================================
FILE: test/console.js
================================================
describe('console', function () {
let tool = eruda.get('console')
tool.config.set('asyncRender', false)
let $tool = $('.eruda-console')
let logger = tool._logger
function log(i) {
return logs()[i].container
}
function logs() {
return logger.displayLogs
}
beforeEach(function () {
eruda.show('console')
logger.clear(true)
})
describe('config', function () {
let config = tool.config
it('override console', function () {
config.set('overrideConsole', true)
console.log('test')
expect($(log(0))).toContainText('test')
})
})
describe('ui', function () {
it('clear', function () {
tool.log('test')
$('.eruda-clear-console').click()
expect($tool.find('.eruda-logs li')).toHaveLength(0)
})
it('level', function () {
tool.log('test')
tool.warn('test')
expect(logs()).toHaveLength(2)
$('.eruda-level[data-level="warning"]').click()
expect(logs()).toHaveLength(1)
$('.eruda-level[data-level="all"]').click()
})
})
describe('execute', function () {
it('js', function () {
$tool.find('textarea').val('1+2')
$('.eruda-execute').click()
expect($(log(1))).toContainText('3')
})
})
describe('events', function () {
it('log', function () {
let sum = 0
function add(num) {
sum += num
}
tool.on('log', add)
tool.log(5)
expect(sum).toBe(5)
tool.log(6)
expect(sum).toBe(11)
tool.off('log', add)
tool.log(1)
expect(sum).toBe(11)
})
})
})
================================================
FILE: test/data.json
================================================
[
{
"name": "Test",
"author": {
"name": "Redhoodsu",
"email": "surunzi@foxmail.com",
"contact": [
{
"location": "office ",
"number": 123456
},
{
"location": "A very very long long address!!!!!!!!!!!!",
"number": 654321
},
null
]
}
}
]
================================================
FILE: test/elements.html
================================================
Elements
================================================
FILE: test/elements.js
================================================
describe('elements', function () {
let tool = eruda.get('elements')
beforeEach(function () {
eruda.show('elements')
})
describe('api', function () {
it('select element', function () {
tool.select(document.body)
})
})
})
================================================
FILE: test/eruda.html
================================================
Features
================================================
FILE: test/eruda.js
================================================
describe('devTools', function () {
describe('init', function () {
it('destroy', function () {
eruda.destroy()
expect($('#eruda')).toHaveLength(0)
})
it('init', function () {
let container = document.createElement('div')
container.id = 'eruda'
document.body.appendChild(container)
eruda.init({
container: container,
tool: [],
useShadowDom: false,
})
let $eruda = $('#eruda')
expect($eruda.find('.eruda-dev-tools')).toHaveLength(1)
})
})
describe('tool', function () {
it('add', function () {
eruda.add({
name: 'test',
init: function ($el) {
this._$el = $el
$el.html('Test Plugin')
},
})
expect($('.eruda-test')).toContainText('Test Plugin')
})
it('show', function () {
let $tool = $('.eruda-test')
expect($tool).toBeHidden()
eruda.show('test')
expect($tool).toHaveCss({ display: 'block' })
})
it('remove', function () {
eruda.remove('test')
expect($('.eruda-test')).toHaveLength(0)
})
})
describe('display', function () {
it('show', function () {
eruda.show()
expect($('.eruda-dev-tools')).toHaveCss({ display: 'block' })
})
it('hide', function (done) {
eruda.hide()
setTimeout(function () {
expect($('.eruda-dev-tools')).toBeHidden()
done()
}, 500)
})
})
describe('scale', function () {
it('get', function () {
eruda.scale(1)
expect(eruda.scale()).toBe(1)
})
})
})
================================================
FILE: test/index.html
================================================
Eruda Test Page
================================================
FILE: test/info.html
================================================
Info
================================================
FILE: test/info.js
================================================
describe('info', function () {
let tool = eruda.get('info')
let $tool = $('.eruda-info')
describe('default', function () {
it('location', function () {
expect($tool.find('.eruda-content').eq(0)).toContainText(location.href)
})
it('user agent', function () {
expect($tool.find('.eruda-content').eq(1)).toContainText(
navigator.userAgent
)
})
it('device', function () {
expect($tool.find('.eruda-content').eq(2)).toContainText(
window.innerWidth
)
})
it('system', function () {
expect($tool.find('.eruda-content').eq(3)).toContainText('os')
})
it('sponsor', function () {
expect($tool.find('.eruda-content').eq(4)).toContainText(
'Open Collective'
)
})
it('about', function () {
expect($tool.find('.eruda-content').eq(5)).toHaveText(/Eruda v[\d.]+/)
})
})
it('clear', function () {
tool.clear()
expect($tool.find('li')).toHaveLength(0)
})
it('add', function () {
tool.add('test', 'eruda')
expect($tool.find('.eruda-title')).toContainText('test')
expect($tool.find('.eruda-content')).toContainText('eruda')
tool.add('test', 'update')
tool.add('test', 'update')
expect($tool.find('.eruda-content')).toContainText('update')
})
it('get', function () {
expect(tool.get()).toEqual([{ name: 'test', val: 'update' }])
expect(tool.get('test')).toBe('update')
expect(tool.get('test2')).not.toBeDefined()
})
it('remove', function () {
tool.remove('test')
expect($tool.find('li')).toHaveLength(0)
})
})
================================================
FILE: test/init.js
================================================
eruda.init({
useShadowDom: false,
})
================================================
FILE: test/inline.html
================================================
Inline
================================================
FILE: test/manual.html
================================================
Manual
================================================
FILE: test/network.html
================================================
Network
================================================
FILE: test/network.js
================================================
describe('network', function () {
beforeEach(function () {
eruda.show('network')
})
describe('request', function () {
it('xhr', function (done) {
$('.eruda-clear-xhr').click()
util.ajax.get(window.location.toString(), function () {
setTimeout(function () {
expect($('.eruda-requests .luna-data-grid-node')).toHaveLength(1)
done()
}, 500)
})
})
})
})
================================================
FILE: test/resources.html
================================================
Resources
================================================
FILE: test/resources.js
================================================
describe('resources', function () {
let $tool = $('.eruda-resources')
beforeEach(function () {
eruda.show('resources')
})
describe('localStorage', function () {
it('show', function () {
localStorage.clear()
localStorage.setItem('testKey', 'testVal')
})
it('clear', function () {
$tool.find('.eruda-local-storage .eruda-clear-storage').click()
})
})
describe('sessionStorage', function () {
it('show', function () {
sessionStorage.clear()
sessionStorage.setItem('testKey', 'testVal')
})
it('clear', function () {
$tool.find('.eruda-session-storage .eruda-clear-storage').click()
})
})
describe('cookie', function () {
it('show', function () {
util.cookie.set('testKey', 'testVal')
$tool.find('.eruda-refresh-cookie').click()
})
it('clear', function () {
$tool.find('.eruda-clear-cookie').click()
})
})
})
================================================
FILE: test/settings.html
================================================
Settings
================================================
FILE: test/settings.js
================================================
describe('settings', function () {
let tool = eruda.get('settings')
let $tool = $('.eruda-settings')
let cfg = eruda.Settings.createCfg('test')
cfg.set({
testSwitch: false,
testSelect: '1',
testRange: 1,
testColor: '#fff',
})
beforeEach(function () {
tool.clear()
})
it('switch', function () {
let text = 'Test Switch'
tool.switch(cfg, 'testSwitch', text)
})
it('separator', function () {
tool.separator()
})
it('select', function () {
let text = 'Test Select'
tool.select(cfg, 'testSelect', text, ['1', '2', '3'])
})
it('range', function () {
let text = 'Test Range'
tool.range(cfg, 'testRange', text, { min: 0, max: 1, step: 0.1 })
})
it('remove', function () {
let text = 'Test Switch'
tool.switch(cfg, 'testSwitch', text)
tool.remove(cfg, 'testSwitch')
})
})
================================================
FILE: test/snippets.html
================================================
Snippets
================================================
FILE: test/snippets.js
================================================
describe('snippets', function () {
let tool = eruda.get('snippets')
let $tool = $('.eruda-snippets')
describe('default', function () {
it('border all', function () {
expect($tool.find('.eruda-name').eq(0)).toContainText('Border All')
let $body = $('body')
let $btn = $tool.find('.eruda-run').eq(0)
$btn.click()
expect($body).toHaveCss({ outlineWidth: '2px' })
$btn.click()
expect($body).toHaveCss({ outlineWidth: '0px' })
})
it('refresh page', function () {
expect($tool.find('.eruda-name').eq(1)).toContainText('Refresh Page')
})
it('search text', function () {
expect($tool.find('.eruda-name').eq(2)).toContainText('Search Text')
})
it('edit page', function () {
expect($tool.find('.eruda-name').eq(3)).toContainText('Edit Page')
let $body = $('body')
let $btn = $tool.find('.eruda-run').eq(3)
$btn.click()
expect($body).toHaveAttr('contenteditable', 'true')
$btn.click()
expect($body).toHaveAttr('contenteditable', 'false')
})
})
it('clear', function () {
tool.clear()
expect($tool.find('.eruda-name')).toHaveLength(0)
})
it('add', function () {
tool.add(
'Test',
function () {
console.log('eruda')
},
'This is the description'
)
expect($tool.find('.eruda-name')).toContainText('Test')
expect($tool.find('.eruda-description')).toContainText(
'This is the description'
)
})
it('remove', function () {
tool.remove('Test')
expect($tool.find('.eruda-name')).toHaveLength(0)
})
})
================================================
FILE: test/sources.html
================================================
Sources
================================================
FILE: test/sources.js
================================================
describe('sources', function () {
let tool = eruda.get('sources')
let $tool = $('.eruda-sources')
beforeEach(function () {
eruda.show('sources')
})
it('raw', function () {
tool.set('raw', '/* test */')
})
})
================================================
FILE: test/style.css
================================================
body, html {
padding: 0;
margin: 0;
font-family: 'Avenir Next', Avenir, 'Helvetica Neue', Helvetica, 'Franklin Gothic Medium', 'Franklin Gothic', 'ITC Franklin Gothic', Arial, sans-serif;
}
header {
position: relative;
z-index: 15;
background: #eda29b;
text-align: center;
color: #fff;
padding: 10px;
font-size: 30px;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.05),0 1px 4px 0 rgba(0,0,0,.2), 0 3px 1px -2px rgba(0,0,0,.1);
}
nav ul {
list-style: none;
padding: 0;
margin: 15px;
}
nav ul li {
background: #f2d367;
width: 50%;
float: left;
}
nav ul li:nth-child(4n), nav ul li:nth-child(4n-3) {
background: #e17555;
}
nav ul li a {
text-align: center;
width: 100%;
height: 100%;
box-sizing: border-box;
display: block;
padding: 10px;
color: #e07556;
font-size: 16px;
text-decoration: none;
}
nav ul li:nth-child(4n) a, nav ul li:nth-child(4n-3) a{
color: #9c3c53;
}
================================================
FILE: test/util.js
================================================
// Built by eustia.
(function(root, factory)
{
if (typeof define === 'function' && define.amd)
{
define([], factory);
} else if (typeof module === 'object' && module.exports)
{
module.exports = factory();
} else { root.util = factory(); }
}(this, function ()
{
/* eslint-disable */
var _ = {};
if (typeof window === 'object' && window.util) _ = window.util;
/* ------------------------------ types ------------------------------ */
var types = _.types = (function (exports) {
/* Used for typescript definitions only.
*/
/* typescript
* export declare namespace types {
* interface Collection {}
* interface List extends Collection {
* [index: number]: T;
* length: number;
* }
* interface ListIterator {
* (value: T, index: number, list: List): TResult;
* }
* interface Dictionary extends Collection {
* [index: string]: T;
* }
* interface ObjectIterator {
* (element: T, key: string, list: Dictionary): TResult;
* }
* interface MemoIterator {
* (prev: TResult, curr: T, index: number, list: List): TResult;
* }
* interface MemoObjectIterator {
* (prev: TResult, curr: T, key: string, list: Dictionary): TResult;
* }
* type Fn = (...args: any[]) => T;
* type AnyFn = Fn;
* type PlainObj = { [name: string]: T };
* }
* export declare const types: {};
*/
exports = {};
return exports;
})({});
/* ------------------------------ noop ------------------------------ */
var noop = _.noop = (function (exports) {
/* A no-operation function.
*/
/* example
* noop(); // Does nothing
*/
/* typescript
* export declare function noop(): void;
*/
exports = function() {};
return exports;
})({});
/* ------------------------------ isObj ------------------------------ */
var isObj = _.isObj = (function (exports) {
/* Check if value is the language type of Object.
*
* |Name |Desc |
* |------|--------------------------|
* |val |Value to check |
* |return|True if value is an object|
*
* [Language Spec](http://www.ecma-international.org/ecma-262/6.0/#sec-ecmascript-language-types)
*/
/* example
* isObj({}); // -> true
* isObj([]); // -> true
*/
/* typescript
* export declare function isObj(val: any): boolean;
*/
exports = function(val) {
var type = typeof val;
return !!val && (type === 'function' || type === 'object');
};
return exports;
})({});
/* ------------------------------ has ------------------------------ */
var has = _.has = (function (exports) {
/* Checks if key is a direct property.
*
* |Name |Desc |
* |------|--------------------------------|
* |obj |Object to query |
* |key |Path to check |
* |return|True if key is a direct property|
*/
/* example
* has({ one: 1 }, 'one'); // -> true
*/
/* typescript
* export declare function has(obj: {}, key: string): boolean;
*/
var hasOwnProp = Object.prototype.hasOwnProperty;
exports = function(obj, key) {
return hasOwnProp.call(obj, key);
};
return exports;
})({});
/* ------------------------------ keys ------------------------------ */
var keys = _.keys = (function (exports) {
/* Create an array of the own enumerable property names of object.
*
* |Name |Desc |
* |------|-----------------------|
* |obj |Object to query |
* |return|Array of property names|
*/
/* example
* keys({ a: 1 }); // -> ['a']
*/
/* typescript
* export declare function keys(obj: any): string[];
*/
/* dependencies
* has
*/
if (Object.keys && !false) {
exports = Object.keys;
} else {
exports = function(obj) {
var ret = [];
for (var key in obj) {
if (has(obj, key)) ret.push(key);
}
return ret;
};
}
return exports;
})({});
/* ------------------------------ chunk ------------------------------ */
var chunk = _.chunk = (function (exports) {
/* Split array into groups the length of given size.
*
* |Name |Desc |
* |------|--------------------|
* |arr |Array to process |
* |size=1|Length of each chunk|
* |return|Chunks of given size|
*/
/* example
* chunk([1, 2, 3, 4], 2); // -> [[1, 2], [3, 4]]
* chunk([1, 2, 3, 4], 3); // -> [[1, 2, 3], [4]]
* chunk([1, 2, 3, 4]); // -> [[1], [2], [3], [4]]
*/
/* typescript
* export declare function chunk(arr: any[], size?: number): Array;
*/
exports = function(arr, size) {
var ret = [];
size = size || 1;
for (var i = 0, len = Math.ceil(arr.length / size); i < len; i++) {
var start = i * size;
var end = start + size;
ret.push(arr.slice(start, end));
}
return ret;
};
return exports;
})({});
/* ------------------------------ idxOf ------------------------------ */
var idxOf = _.idxOf = (function (exports) {
/* Get the index at which the first occurrence of value.
*
* |Name |Desc |
* |---------|--------------------|
* |arr |Array to search |
* |val |Value to search for |
* |fromIdx=0|Index to search from|
* |return |Value index |
*/
/* example
* idxOf([1, 2, 1, 2], 2, 2); // -> 3
*/
/* typescript
* export declare function idxOf(arr: any[], val: any, fromIdx?: number): number;
*/
exports = function(arr, val, fromIdx) {
return Array.prototype.indexOf.call(arr, val, fromIdx);
};
return exports;
})({});
/* ------------------------------ isUndef ------------------------------ */
var isUndef = _.isUndef = (function (exports) {
/* Check if value is undefined.
*
* |Name |Desc |
* |------|--------------------------|
* |val |Value to check |
* |return|True if value is undefined|
*/
/* example
* isUndef(void 0); // -> true
* isUndef(null); // -> false
*/
/* typescript
* export declare function isUndef(val: any): boolean;
*/
exports = function(val) {
return val === void 0;
};
return exports;
})({});
/* ------------------------------ create ------------------------------ */
var create = _.create = (function (exports) {
/* Create new object using given object as prototype.
*
* |Name |Desc |
* |------|-----------------------|
* |proto |Prototype of new object|
* |return|Created object |
*/
/* example
* const obj = create({ a: 1 });
* console.log(obj.a); // -> 1
*/
/* typescript
* export declare function create(proto?: object): any;
*/
/* dependencies
* isObj
*/
exports = function(proto) {
if (!isObj(proto)) return {};
if (objCreate && !false) return objCreate(proto);
function noop() {}
noop.prototype = proto;
return new noop();
};
var objCreate = Object.create;
return exports;
})({});
/* ------------------------------ inherits ------------------------------ */
var inherits = _.inherits = (function (exports) {
/* Inherit the prototype methods from one constructor into another.
*
* |Name |Desc |
* |----------|-----------|
* |Class |Child Class|
* |SuperClass|Super Class|
*/
/* example
* function People(name) {
* this._name = name;
* }
* People.prototype = {
* getName: function() {
* return this._name;
* }
* };
* function Student(name) {
* this._name = name;
* }
* inherits(Student, People);
* const s = new Student('RedHood');
* s.getName(); // -> 'RedHood'
*/
/* typescript
* export declare function inherits(
* Class: types.AnyFn,
* SuperClass: types.AnyFn
* ): void;
*/
/* dependencies
* create types
*/
exports = function(Class, SuperClass) {
Class.prototype = create(SuperClass.prototype);
};
return exports;
})({});
/* ------------------------------ restArgs ------------------------------ */
var restArgs = _.restArgs = (function (exports) {
/* This accumulates the arguments passed into an array, after a given index.
*
* |Name |Desc |
* |----------|---------------------------------------|
* |function |Function that needs rest parameters |
* |startIndex|The start index to accumulates |
* |return |Generated function with rest parameters|
*/
/* example
* const paramArr = restArgs(function(rest) {
* return rest;
* });
* paramArr(1, 2, 3, 4); // -> [1, 2, 3, 4]
*/
/* typescript
* export declare function restArgs(
* fn: types.AnyFn,
* startIndex?: number
* ): types.AnyFn;
*/
/* dependencies
* types
*/
exports = function(fn, startIdx) {
startIdx = startIdx == null ? fn.length - 1 : +startIdx;
return function() {
var len = Math.max(arguments.length - startIdx, 0);
var rest = new Array(len);
var i;
for (i = 0; i < len; i++) {
rest[i] = arguments[i + startIdx];
} // Call runs faster than apply.
switch (startIdx) {
case 0:
return fn.call(this, rest);
case 1:
return fn.call(this, arguments[0], rest);
case 2:
return fn.call(this, arguments[0], arguments[1], rest);
}
var args = new Array(startIdx + 1);
for (i = 0; i < startIdx; i++) {
args[i] = arguments[i];
}
args[startIdx] = rest;
return fn.apply(this, args);
};
};
return exports;
})({});
/* ------------------------------ optimizeCb ------------------------------ */
var optimizeCb = _.optimizeCb = (function (exports) {
/* Used for function context binding.
*/
/* typescript
* export declare function optimizeCb(
* fn: types.AnyFn,
* ctx: any,
* argCount?: number
* ): types.AnyFn;
*/
/* dependencies
* isUndef types
*/
exports = function(fn, ctx, argCount) {
if (isUndef(ctx)) return fn;
switch (argCount == null ? 3 : argCount) {
case 1:
return function(val) {
return fn.call(ctx, val);
};
case 3:
return function(val, idx, collection) {
return fn.call(ctx, val, idx, collection);
};
case 4:
return function(accumulator, val, idx, collection) {
return fn.call(ctx, accumulator, val, idx, collection);
};
}
return function() {
return fn.apply(ctx, arguments);
};
};
return exports;
})({});
/* ------------------------------ endWith ------------------------------ */
var endWith = _.endWith = (function (exports) {
/* Check if string ends with the given target string.
*
* |Name |Desc |
* |------|-------------------------------|
* |str |The string to search |
* |suffix|String suffix |
* |return|True if string ends with target|
*/
/* example
* endWith('ab', 'b'); // -> true
*/
/* typescript
* export declare function endWith(str: string, suffix: string): boolean;
*/
exports = function(str, suffix) {
var idx = str.length - suffix.length;
return idx >= 0 && str.indexOf(suffix, idx) === idx;
};
return exports;
})({});
/* ------------------------------ toStr ------------------------------ */
var toStr = _.toStr = (function (exports) {
/* Convert value to a string.
*
* |Name |Desc |
* |------|----------------|
* |val |Value to convert|
* |return|Result string |
*/
/* example
* toStr(null); // -> ''
* toStr(1); // -> '1'
* toStr(false); // -> 'false'
* toStr([1, 2, 3]); // -> '1,2,3'
*/
/* typescript
* export declare function toStr(val: any): string;
*/
exports = function(val) {
return val == null ? '' : val.toString();
};
return exports;
})({});
/* ------------------------------ escapeJsStr ------------------------------ */
var escapeJsStr = _.escapeJsStr = (function (exports) {
/* Escape string to be a valid JavaScript string literal between quotes.
*
* http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.4
*
* |Name |Desc |
* |------|----------------|
* |str |String to escape|
* |return|Escaped string |
*/
/* example
* escapeJsStr('"\n'); // -> '\\"\\\\n'
*/
/* typescript
* export declare function escapeJsStr(str: string): string;
*/
/* dependencies
* toStr
*/
exports = function(str) {
return toStr(str).replace(regEscapeChars, function(char) {
switch (char) {
case '"':
case "'":
case '\\':
return '\\' + char;
case '\n':
return '\\n';
case '\r':
return '\\r';
// Line separator
case '\u2028':
return '\\u2028';
// Paragraph separator
case '\u2029':
return '\\u2029';
}
});
};
var regEscapeChars = /["'\\\n\r\u2028\u2029]/g;
return exports;
})({});
/* ------------------------------ evalCss ------------------------------ */
_.evalCss = (function (exports) {
/* Load css into page.
*
* |Name |Desc |
* |------|-------------|
* |css |Css code |
* |return|Style element|
*/
/* example
* evalCss('body{background:#08c}');
*/
/* typescript
* export declare function evalCss(css: string): HTMLStyleElement;
*/
exports = function(css) {
var style = document.createElement('style');
style.textContent = css;
style.type = 'text/css';
document.head.appendChild(style);
return style;
};
return exports;
})({});
/* ------------------------------ identity ------------------------------ */
var identity = _.identity = (function (exports) {
/* Return the first argument given.
*
* |Name |Desc |
* |------|-----------|
* |val |Any value |
* |return|Given value|
*/
/* example
* identity('a'); // -> 'a'
*/
/* typescript
* export declare function identity(val: T): T;
*/
exports = function(val) {
return val;
};
return exports;
})({});
/* ------------------------------ objToStr ------------------------------ */
var objToStr = _.objToStr = (function (exports) {
/* Alias of Object.prototype.toString.
*
* |Name |Desc |
* |------|------------------------------------|
* |val |Source value |
* |return|String representation of given value|
*/
/* example
* objToStr(5); // -> '[object Number]'
*/
/* typescript
* export declare function objToStr(val: any): string;
*/
var ObjToStr = Object.prototype.toString;
exports = function(val) {
return ObjToStr.call(val);
};
return exports;
})({});
/* ------------------------------ isArgs ------------------------------ */
var isArgs = _.isArgs = (function (exports) {
/* Check if value is classified as an arguments object.
*
* |Name |Desc |
* |------|------------------------------------|
* |val |Value to check |
* |return|True if value is an arguments object|
*/
/* example
* isArgs(
* (function() {
* return arguments;
* })()
* ); // -> true
*/
/* typescript
* export declare function isArgs(val: any): boolean;
*/
/* dependencies
* objToStr
*/
exports = function(val) {
return objToStr(val) === '[object Arguments]';
};
return exports;
})({});
/* ------------------------------ isArr ------------------------------ */
var isArr = _.isArr = (function (exports) {
/* Check if value is an `Array` object.
*
* |Name |Desc |
* |------|----------------------------------|
* |val |Value to check |
* |return|True if value is an `Array` object|
*/
/* example
* isArr([]); // -> true
* isArr({}); // -> false
*/
/* typescript
* export declare function isArr(val: any): boolean;
*/
/* dependencies
* objToStr
*/
if (Array.isArray && !false) {
exports = Array.isArray;
} else {
exports = function(val) {
return objToStr(val) === '[object Array]';
};
}
return exports;
})({});
/* ------------------------------ castPath ------------------------------ */
var castPath = _.castPath = (function (exports) {
/* Cast value into a property path array.
*
* |Name |Desc |
* |------|-------------------|
* |path |Value to inspect |
* |obj |Object to query |
* |return|Property path array|
*/
/* example
* castPath('a.b.c'); // -> ['a', 'b', 'c']
* castPath(['a']); // -> ['a']
* castPath('a[0].b'); // -> ['a', '0', 'b']
* castPath('a.b.c', { 'a.b.c': true }); // -> ['a.b.c']
*/
/* typescript
* export declare function castPath(path: string | string[], obj?: any): string[];
*/
/* dependencies
* has isArr
*/
exports = function(str, obj) {
if (isArr(str)) return str;
if (obj && has(obj, str)) return [str];
var ret = [];
str.replace(regPropName, function(match, number, quote, str) {
ret.push(quote ? str.replace(regEscapeChar, '$1') : number || match);
});
return ret;
}; // Lodash _stringToPath
var regPropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g;
var regEscapeChar = /\\(\\)?/g;
return exports;
})({});
/* ------------------------------ safeGet ------------------------------ */
var safeGet = _.safeGet = (function (exports) {
/* Get object property, don't throw undefined error.
*
* |Name |Desc |
* |------|-------------------------|
* |obj |Object to query |
* |path |Path of property to get |
* |return|Target value or undefined|
*/
/* example
* const obj = { a: { aa: { aaa: 1 } } };
* safeGet(obj, 'a.aa.aaa'); // -> 1
* safeGet(obj, ['a', 'aa']); // -> {aaa: 1}
* safeGet(obj, 'a.b'); // -> undefined
*/
/* typescript
* export declare function safeGet(obj: any, path: string | string[]): any;
*/
/* dependencies
* isUndef castPath
*/
exports = function(obj, path) {
path = castPath(path, obj);
var prop;
prop = path.shift();
while (!isUndef(prop)) {
obj = obj[prop];
if (obj == null) return;
prop = path.shift();
}
return obj;
};
return exports;
})({});
/* ------------------------------ flatten ------------------------------ */
var flatten = _.flatten = (function (exports) {
/* Recursively flatten an array.
*
* |Name |Desc |
* |------|-------------------|
* |arr |Array to flatten |
* |return|New flattened array|
*/
/* example
* flatten(['a', ['b', ['c']], 'd', ['e']]); // -> ['a', 'b', 'c', 'd', 'e']
*/
/* typescript
* export declare function flatten(arr: any[]): any[];
*/
/* dependencies
* isArr
*/
exports = function(arr) {
return flat(arr, []);
};
function flat(arr, res) {
var len = arr.length,
i = -1,
cur;
while (len--) {
cur = arr[++i];
isArr(cur) ? flat(cur, res) : res.push(cur);
}
return res;
}
return exports;
})({});
/* ------------------------------ isFn ------------------------------ */
var isFn = _.isFn = (function (exports) {
/* Check if value is a function.
*
* |Name |Desc |
* |------|---------------------------|
* |val |Value to check |
* |return|True if value is a function|
*
* Generator function is also classified as true.
*/
/* example
* isFn(function() {}); // -> true
* isFn(function*() {}); // -> true
* isFn(async function() {}); // -> true
*/
/* typescript
* export declare function isFn(val: any): boolean;
*/
/* dependencies
* objToStr
*/
exports = function(val) {
var objStr = objToStr(val);
return (
objStr === '[object Function]' ||
objStr === '[object GeneratorFunction]' ||
objStr === '[object AsyncFunction]'
);
};
return exports;
})({});
/* ------------------------------ getProto ------------------------------ */
var getProto = _.getProto = (function (exports) {
/* Get prototype of an object.
*
* |Name |Desc |
* |------|---------------------------------------------|
* |obj |Target object |
* |return|Prototype of given object, null if not exists|
*/
/* example
* const a = {};
* getProto(Object.create(a)); // -> a
*/
/* typescript
* export declare function getProto(obj: any): any;
*/
/* dependencies
* isObj isFn
*/
var getPrototypeOf = Object.getPrototypeOf;
var ObjectCtr = {}.constructor;
exports = function(obj) {
if (!isObj(obj)) return;
if (getPrototypeOf && !false) return getPrototypeOf(obj);
var proto = obj.__proto__;
if (proto || proto === null) return proto;
if (isFn(obj.constructor)) return obj.constructor.prototype;
if (obj instanceof ObjectCtr) return ObjectCtr.prototype;
};
return exports;
})({});
/* ------------------------------ isMiniProgram ------------------------------ */
var isMiniProgram = _.isMiniProgram = (function (exports) {
/* Check if running in wechat mini program.
*/
/* example
* console.log(isMiniProgram); // -> true if running in mini program.
*/
/* typescript
* export declare const isMiniProgram: boolean;
*/
/* dependencies
* isFn
*/
/* eslint-disable no-undef */
exports = typeof wx !== 'undefined' && isFn(wx.openLocation);
return exports;
})({});
/* ------------------------------ isNum ------------------------------ */
var isNum = _.isNum = (function (exports) {
/* Check if value is classified as a Number primitive or object.
*
* |Name |Desc |
* |------|-------------------------------------|
* |val |Value to check |
* |return|True if value is correctly classified|
*/
/* example
* isNum(5); // -> true
* isNum(5.1); // -> true
* isNum({}); // -> false
*/
/* typescript
* export declare function isNum(val: any): boolean;
*/
/* dependencies
* objToStr
*/
exports = function(val) {
return objToStr(val) === '[object Number]';
};
return exports;
})({});
/* ------------------------------ isArrLike ------------------------------ */
var isArrLike = _.isArrLike = (function (exports) {
/* Check if value is array-like.
*
* |Name |Desc |
* |------|---------------------------|
* |val |Value to check |
* |return|True if value is array like|
*
* Function returns false.
*/
/* example
* isArrLike('test'); // -> true
* isArrLike(document.body.children); // -> true;
* isArrLike([1, 2, 3]); // -> true
*/
/* typescript
* export declare function isArrLike(val: any): boolean;
*/
/* dependencies
* isNum isFn
*/
var MAX_ARR_IDX = Math.pow(2, 53) - 1;
exports = function(val) {
if (!val) return false;
var len = val.length;
return isNum(len) && len >= 0 && len <= MAX_ARR_IDX && !isFn(val);
};
return exports;
})({});
/* ------------------------------ each ------------------------------ */
var each = _.each = (function (exports) {
/* Iterate over elements of collection and invokes iterator for each element.
*
* |Name |Desc |
* |--------|------------------------------|
* |obj |Collection to iterate over |
* |iterator|Function invoked per iteration|
* |ctx |Function context |
*/
/* example
* each({ a: 1, b: 2 }, function(val, key) {});
*/
/* typescript
* export declare function each(
* list: types.List,
* iterator: types.ListIterator,
* ctx?: any
* ): types.List;
* export declare function each(
* object: types.Dictionary,
* iterator: types.ObjectIterator,
* ctx?: any
* ): types.Collection;
*/
/* dependencies
* isArrLike keys optimizeCb types
*/
exports = function(obj, iterator, ctx) {
iterator = optimizeCb(iterator, ctx);
var i, len;
if (isArrLike(obj)) {
for (i = 0, len = obj.length; i < len; i++) {
iterator(obj[i], i, obj);
}
} else {
var _keys = keys(obj);
for (i = 0, len = _keys.length; i < len; i++) {
iterator(obj[_keys[i]], _keys[i], obj);
}
}
return obj;
};
return exports;
})({});
/* ------------------------------ createAssigner ------------------------------ */
var createAssigner = _.createAssigner = (function (exports) {
/* Used to create extend, extendOwn and defaults.
*
* |Name |Desc |
* |--------|------------------------------|
* |keysFn |Function to get object keys |
* |defaults|No override when set to true |
* |return |Result function, extend... |
*/
/* typescript
* export declare function createAssigner(
* keysFn: types.AnyFn,
* defaults: boolean
* ): types.AnyFn;
*/
/* dependencies
* isUndef each types
*/
exports = function(keysFn, defaults) {
return function(obj) {
each(arguments, function(src, idx) {
if (idx === 0) return;
var keys = keysFn(src);
each(keys, function(key) {
if (!defaults || isUndef(obj[key])) obj[key] = src[key];
});
});
return obj;
};
};
return exports;
})({});
/* ------------------------------ extendOwn ------------------------------ */
var extendOwn = _.extendOwn = (function (exports) {
/* Like extend, but only copies own properties over to the destination object.
*
* |Name |Desc |
* |-----------|------------------|
* |destination|Destination object|
* |...sources |Sources objects |
* |return |Destination object|
*/
/* example
* extendOwn({ name: 'RedHood' }, { age: 24 }); // -> {name: 'RedHood', age: 24}
*/
/* typescript
* export declare function extendOwn(destination: any, ...sources: any[]): any;
*/
/* dependencies
* keys createAssigner
*/
exports = createAssigner(keys);
return exports;
})({});
/* ------------------------------ values ------------------------------ */
var values = _.values = (function (exports) {
/* Create an array of the own enumerable property values of object.
*
* |Name |Desc |
* |------|------------------------|
* |obj |Object to query |
* |return|Array of property values|
*/
/* example
* values({ one: 1, two: 2 }); // -> [1, 2]
*/
/* typescript
* export declare function values(obj: any): any[];
*/
/* dependencies
* each
*/
exports = function(obj) {
var ret = [];
each(obj, function(val) {
ret.push(val);
});
return ret;
};
return exports;
})({});
/* ------------------------------ isStr ------------------------------ */
var isStr = _.isStr = (function (exports) {
/* Check if value is a string primitive.
*
* |Name |Desc |
* |------|-----------------------------------|
* |val |Value to check |
* |return|True if value is a string primitive|
*/
/* example
* isStr('licia'); // -> true
*/
/* typescript
* export declare function isStr(val: any): boolean;
*/
/* dependencies
* objToStr
*/
exports = function(val) {
return objToStr(val) === '[object String]';
};
return exports;
})({});
/* ------------------------------ contain ------------------------------ */
var contain = _.contain = (function (exports) {
/* Check if the value is present in the list.
*
* |Name |Desc |
* |------|------------------------------------|
* |target|Target object |
* |val |Value to check |
* |return|True if value is present in the list|
*/
/* example
* contain([1, 2, 3], 1); // -> true
* contain({ a: 1, b: 2 }, 1); // -> true
* contain('abc', 'a'); // -> true
*/
/* typescript
* export declare function contain(arr: any[] | {} | string, val: any): boolean;
*/
/* dependencies
* idxOf isStr isArrLike values
*/
exports = function(arr, val) {
if (isStr(arr)) return arr.indexOf(val) > -1;
if (!isArrLike(arr)) arr = values(arr);
return idxOf(arr, val) >= 0;
};
return exports;
})({});
/* ------------------------------ defineProp ------------------------------ */
var defineProp = _.defineProp = (function (exports) {
/* Shortcut for Object.defineProperty(defineProperties).
*
* |Name |Desc |
* |----------|-------------------|
* |obj |Object to define |
* |prop |Property path |
* |descriptor|Property descriptor|
* |return |Object itself |
*
* |Name |Desc |
* |------|--------------------|
* |obj |Object to define |
* |prop |Property descriptors|
* |return|Object itself |
*/
/* example
* const obj = { b: { c: 3 }, d: 4, e: 5 };
* defineProp(obj, 'a', {
* get: function() {
* return this.e * 2;
* }
* });
* // obj.a is equal to 10
* defineProp(obj, 'b.c', {
* set: function(val) {
* // this is pointed to obj.b
* this.e = val;
* }.bind(obj)
* });
* obj.b.c = 2;
* // obj.a is equal to 4
*
* const obj2 = { a: 1, b: 2, c: 3 };
* defineProp(obj2, {
* a: {
* get: function() {
* return this.c;
* }
* },
* b: {
* set: function(val) {
* this.c = val / 2;
* }
* }
* });
* // obj2.a is equal to 3
* obj2.b = 4;
* // obj2.a is equal to 2
*/
/* typescript
* export declare function defineProp(
* obj: T,
* prop: string,
* descriptor: PropertyDescriptor
* ): T;
* export declare function defineProp(
* obj: T,
* descriptor: PropertyDescriptorMap
* ): T;
*/
/* dependencies
* castPath isStr isObj each
*/
exports = function(obj, prop, descriptor) {
if (isStr(prop)) {
defineProp(obj, prop, descriptor);
} else if (isObj(prop)) {
each(prop, function(descriptor, prop) {
defineProp(obj, prop, descriptor);
});
}
return obj;
};
function defineProp(obj, prop, descriptor) {
var path = castPath(prop, obj);
var lastProp = path.pop();
/* eslint-disable no-cond-assign */
while ((prop = path.shift())) {
if (!obj[prop]) obj[prop] = {};
obj = obj[prop];
}
Object.defineProperty(obj, lastProp, descriptor);
}
return exports;
})({});
/* ------------------------------ isBuffer ------------------------------ */
var isBuffer = _.isBuffer = (function (exports) {
/* Check if value is a buffer.
*
* |Name |Desc |
* |------|-------------------------|
* |val |The value to check |
* |return|True if value is a buffer|
*/
/* example
* isBuffer(new Buffer(4)); // -> true
*/
/* typescript
* export declare function isBuffer(val: any): boolean;
*/
/* dependencies
* isFn
*/
exports = function(val) {
if (val == null) return false;
if (val._isBuffer) return true;
return (
val.constructor &&
isFn(val.constructor.isBuffer) &&
val.constructor.isBuffer(val)
);
};
return exports;
})({});
/* ------------------------------ isEmpty ------------------------------ */
var isEmpty = _.isEmpty = (function (exports) {
/* Check if value is an empty object or array.
*
* |Name |Desc |
* |------|----------------------|
* |val |Value to check |
* |return|True if value is empty|
*/
/* example
* isEmpty([]); // -> true
* isEmpty({}); // -> true
* isEmpty(''); // -> true
*/
/* typescript
* export declare function isEmpty(val: any): boolean;
*/
/* dependencies
* isArrLike isArr isStr isArgs keys
*/
exports = function(val) {
if (val == null) return true;
if (isArrLike(val) && (isArr(val) || isStr(val) || isArgs(val))) {
return val.length === 0;
}
return keys(val).length === 0;
};
return exports;
})({});
/* ------------------------------ isMatch ------------------------------ */
var isMatch = _.isMatch = (function (exports) {
/* Check if keys and values in src are contained in obj.
*
* |Name |Desc |
* |------|----------------------------------|
* |obj |Object to inspect |
* |src |Object of property values to match|
* |return|True if object is match |
*/
/* example
* isMatch({ a: 1, b: 2 }, { a: 1 }); // -> true
*/
/* typescript
* export declare function isMatch(obj: any, src: any): boolean;
*/
/* dependencies
* keys
*/
exports = function(obj, src) {
var _keys = keys(src);
var len = _keys.length;
if (obj == null) return !len;
obj = Object(obj);
for (var i = 0; i < len; i++) {
var key = _keys[i];
if (src[key] !== obj[key] || !(key in obj)) return false;
}
return true;
};
return exports;
})({});
/* ------------------------------ isNaN ------------------------------ */
var isNaN = _.isNaN = (function (exports) {
/* Check if value is an NaN.
*
* |Name |Desc |
* |------|-----------------------|
* |val |Value to check |
* |return|True if value is an NaN|
*
* Undefined is not an NaN, different from global isNaN function.
*/
/* example
* isNaN(0); // -> false
* isNaN(NaN); // -> true
*/
/* typescript
* export declare function isNaN(val: any): boolean;
*/
/* dependencies
* isNum
*/
exports = function(val) {
return isNum(val) && val !== +val;
};
return exports;
})({});
/* ------------------------------ isNil ------------------------------ */
var isNil = _.isNil = (function (exports) {
/* Check if value is null or undefined, the same as value == null.
*
* |Name |Desc |
* |------|----------------------------------|
* |val |Value to check |
* |return|True if value is null or undefined|
*/
/* example
* isNil(null); // -> true
* isNil(void 0); // -> true
* isNil(undefined); // -> true
* isNil(false); // -> false
* isNil(0); // -> false
* isNil([]); // -> false
*/
/* typescript
* export declare function isNil(val: any): boolean;
*/
exports = function(val) {
return val == null;
};
return exports;
})({});
/* ------------------------------ isPromise ------------------------------ */
var isPromise = _.isPromise = (function (exports) {
/* Check if value looks like a promise.
*
* |Name |Desc |
* |------|----------------------------------|
* |val |Value to check |
* |return|True if value looks like a promise|
*/
/* example
* isPromise(new Promise(function() {})); // -> true
* isPromise({}); // -> false
*/
/* typescript
* export declare function isPromise(val: any): boolean;
*/
/* dependencies
* isObj isFn
*/
exports = function(val) {
return isObj(val) && isFn(val.then) && isFn(val.catch);
};
return exports;
})({});
/* ------------------------------ isSymbol ------------------------------ */
var isSymbol = _.isSymbol = (function (exports) {
/* Check if value is a symbol.
*
* |Name |Desc |
* |------|-------------------------|
* |val |Value to check |
* |return|True if value is a symbol|
*/
/* example
* isSymbol(Symbol('test')); // -> true
*/
/* typescript
* export declare function isSymbol(val: any): boolean;
*/
exports = function(val) {
return typeof val === 'symbol';
};
return exports;
})({});
/* ------------------------------ lowerCase ------------------------------ */
var lowerCase = _.lowerCase = (function (exports) {
/* Convert string to lower case.
*
* |Name |Desc |
* |------|------------------|
* |str |String to convert |
* |return|Lower cased string|
*/
/* example
* lowerCase('TEST'); // -> 'test'
*/
/* typescript
* export declare function lowerCase(str: string): string;
*/
/* dependencies
* toStr
*/
exports = function(str) {
return toStr(str).toLocaleLowerCase();
};
return exports;
})({});
/* ------------------------------ ltrim ------------------------------ */
var ltrim = _.ltrim = (function (exports) {
/* Remove chars or white-spaces from beginning of string.
*
* |Name |Desc |
* |------|------------------|
* |str |String to trim |
* |chars |Characters to trim|
* |return|Trimmed string |
*/
/* example
* ltrim(' abc '); // -> 'abc '
* ltrim('_abc_', '_'); // -> 'abc_'
* ltrim('_abc_', ['a', '_']); // -> 'bc_'
*/
/* typescript
* export declare function ltrim(str: string, chars?: string | string[]): string;
*/
var regSpace = /^\s+/;
exports = function(str, chars) {
if (chars == null) {
if (str.trimLeft) {
return str.trimLeft();
}
return str.replace(regSpace, '');
}
var start = 0;
var len = str.length;
var charLen = chars.length;
var found = true;
var i;
var c;
while (found && start < len) {
found = false;
i = -1;
c = str.charAt(start);
while (++i < charLen) {
if (c === chars[i]) {
found = true;
start++;
break;
}
}
}
return start >= len ? '' : str.substr(start, len);
};
return exports;
})({});
/* ------------------------------ matcher ------------------------------ */
var matcher = _.matcher = (function (exports) {
/* Return a predicate function that checks if attrs are contained in an object.
*
* |Name |Desc |
* |------|----------------------------------|
* |attrs |Object of property values to match|
* |return|New predicate function |
*/
/* example
* const filter = require('licia/filter');
*
* const objects = [
* { a: 1, b: 2, c: 3 },
* { a: 4, b: 5, c: 6 }
* ];
* filter(objects, matcher({ a: 4, c: 6 })); // -> [{a: 4, b: 5, c: 6}]
*/
/* typescript
* export declare function matcher(attrs: any): types.AnyFn;
*/
/* dependencies
* extendOwn isMatch types
*/
exports = function(attrs) {
attrs = extendOwn({}, attrs);
return function(obj) {
return isMatch(obj, attrs);
};
};
return exports;
})({});
/* ------------------------------ now ------------------------------ */
var now = _.now = (function (exports) {
/* Gets the number of milliseconds that have elapsed since the Unix epoch.
*/
/* example
* now(); // -> 1468826678701
*/
/* typescript
* export declare function now(): number;
*/
if (Date.now && !false) {
exports = Date.now;
} else {
exports = function() {
return new Date().getTime();
};
}
return exports;
})({});
/* ------------------------------ pick ------------------------------ */
var pick = _.pick = (function (exports) {
/* Return a filtered copy of an object.
*
* |Name |Desc |
* |------|---------------|
* |object|Source object |
* |filter|Object filter |
* |return|Filtered object|
*/
/* example
* pick({ a: 1, b: 2 }, 'a'); // -> {a: 1}
* pick({ a: 1, b: 2, c: 3 }, ['b', 'c']); // -> {b: 2, c: 3}
* pick({ a: 1, b: 2, c: 3, d: 4 }, function(val, key) {
* return val % 2;
* }); // -> {a: 1, c: 3}
*/
/* typescript
* export declare function pick(
* object: any,
* filter: string | string[] | Function
* ): any;
*/
/* dependencies
* isStr isArr contain each
*/
exports = function(obj, filter, omit) {
if (isStr(filter)) filter = [filter];
if (isArr(filter)) {
var keys = filter;
filter = function(val, key) {
return contain(keys, key);
};
}
var ret = {};
var iteratee = function(val, key) {
if (filter(val, key)) ret[key] = val;
};
if (omit) {
iteratee = function(val, key) {
if (!filter(val, key)) ret[key] = val;
};
}
each(obj, iteratee);
return ret;
};
return exports;
})({});
/* ------------------------------ property ------------------------------ */
var property = _.property = (function (exports) {
/* Return a function that will itself return the key property of any passed-in object.
*
* |Name |Desc |
* |------|---------------------------|
* |path |Path of the property to get|
* |return|New accessor function |
*/
/* example
* const obj = { a: { b: 1 } };
* property('a')(obj); // -> {b: 1}
* property(['a', 'b'])(obj); // -> 1
*/
/* typescript
* export declare function property(path: string | string[]): types.AnyFn;
*/
/* dependencies
* isArr safeGet types
*/
exports = function(path) {
if (!isArr(path)) return shallowProperty(path);
return function(obj) {
return safeGet(obj, path);
};
};
function shallowProperty(key) {
return function(obj) {
return obj == null ? void 0 : obj[key];
};
}
return exports;
})({});
/* ------------------------------ safeCb ------------------------------ */
var safeCb = _.safeCb = (function (exports) {
/* Create callback based on input value.
*/
/* typescript
* export declare function safeCb(
* val?: any,
* ctx?: any,
* argCount?: number
* ): types.AnyFn;
*/
/* dependencies
* isFn isObj isArr optimizeCb matcher identity types property
*/
exports = function(val, ctx, argCount) {
if (val == null) return identity;
if (isFn(val)) return optimizeCb(val, ctx, argCount);
if (isObj(val) && !isArr(val)) return matcher(val);
return property(val);
};
return exports;
})({});
/* ------------------------------ filter ------------------------------ */
var filter = _.filter = (function (exports) {
/* Iterates over elements of collection, returning an array of all the values that pass a truth test.
*
* |Name |Desc |
* |---------|---------------------------------------|
* |obj |Collection to iterate over |
* |predicate|Function invoked per iteration |
* |ctx |Predicate context |
* |return |Array of all values that pass predicate|
*/
/* example
* filter([1, 2, 3, 4, 5], function(val) {
* return val % 2 === 0;
* }); // -> [2, 4]
*/
/* typescript
* export declare function filter(
* list: types.List,
* iterator: types.ListIterator,
* context?: any
* ): T[];
* export declare function filter(
* object: types.Dictionary,
* iterator: types.ObjectIterator,
* context?: any
* ): T[];
*/
/* dependencies
* safeCb each types
*/
exports = function(obj, predicate, ctx) {
var ret = [];
predicate = safeCb(predicate, ctx);
each(obj, function(val, idx, list) {
if (predicate(val, idx, list)) ret.push(val);
});
return ret;
};
return exports;
})({});
/* ------------------------------ difference ------------------------------ */
var difference = _.difference = (function (exports) {
/* Create an array of unique array values not included in the other given array.
*
* |Name |Desc |
* |-------|----------------------------|
* |arr |Array to inspect |
* |...args|Values to exclude |
* |return |New array of filtered values|
*/
/* example
* difference([3, 2, 1], [4, 2]); // -> [3, 1]
*/
/* typescript
* export declare function difference(arr: any[], ...args: any[]): any[];
*/
/* dependencies
* restArgs flatten filter contain
*/
exports = restArgs(function(arr, args) {
args = flatten(args);
return filter(arr, function(val) {
return !contain(args, val);
});
});
return exports;
})({});
/* ------------------------------ unique ------------------------------ */
var unique = _.unique = (function (exports) {
/* Create duplicate-free version of an array.
*
* |Name |Desc |
* |------|-----------------------------|
* |arr |Array to inspect |
* |cmp |Function for comparing values|
* |return|New duplicate free array |
*/
/* example
* unique([1, 2, 3, 1]); // -> [1, 2, 3]
*/
/* typescript
* export declare function unique(
* arr: any[],
* cmp?: (a: any, b: any) => boolean | number
* ): any[];
*/
/* dependencies
* filter
*/
exports = function(arr, cmp) {
cmp = cmp || isEqual;
return filter(arr, function(item, idx, arr) {
var len = arr.length;
while (++idx < len) {
if (cmp(item, arr[idx])) return false;
}
return true;
});
};
function isEqual(a, b) {
return a === b;
}
return exports;
})({});
/* ------------------------------ allKeys ------------------------------ */
var allKeys = _.allKeys = (function (exports) {
/* Retrieve all the names of object's own and inherited properties.
*
* |Name |Desc |
* |-------|---------------------------|
* |obj |Object to query |
* |options|Options |
* |return |Array of all property names|
*
* Available options:
*
* |Name |Desc |
* |------------------|-------------------------|
* |prototype=true |Include prototype keys |
* |unenumerable=false|Include unenumerable keys|
* |symbol=false |Include symbol keys |
*
* Members of Object's prototype won't be retrieved.
*/
/* example
* const obj = Object.create({ zero: 0 });
* obj.one = 1;
* allKeys(obj); // -> ['zero', 'one']
*/
/* typescript
* export declare namespace allKeys {
* interface IOptions {
* prototype?: boolean;
* unenumerable?: boolean;
* }
* }
* export declare function allKeys(
* obj: any,
* options: { symbol: true } & allKeys.IOptions
* ): Array;
* export declare function allKeys(
* obj: any,
* options?: ({ symbol: false } & allKeys.IOptions) | allKeys.IOptions
* ): string[];
*/
/* dependencies
* keys getProto unique
*/
var getOwnPropertyNames = Object.getOwnPropertyNames;
var getOwnPropertySymbols = Object.getOwnPropertySymbols;
exports = function(obj) {
var _ref =
arguments.length > 1 && arguments[1] !== undefined
? arguments[1]
: {},
_ref$prototype = _ref.prototype,
prototype = _ref$prototype === void 0 ? true : _ref$prototype,
_ref$unenumerable = _ref.unenumerable,
unenumerable = _ref$unenumerable === void 0 ? false : _ref$unenumerable,
_ref$symbol = _ref.symbol,
symbol = _ref$symbol === void 0 ? false : _ref$symbol;
var ret = [];
if ((unenumerable || symbol) && getOwnPropertyNames) {
var getKeys = keys;
if (unenumerable && getOwnPropertyNames) getKeys = getOwnPropertyNames;
do {
ret = ret.concat(getKeys(obj));
if (symbol && getOwnPropertySymbols) {
ret = ret.concat(getOwnPropertySymbols(obj));
}
} while (
prototype &&
(obj = getProto(obj)) &&
obj !== Object.prototype
);
ret = unique(ret);
} else {
if (prototype) {
for (var key in obj) {
ret.push(key);
}
} else {
ret = keys(obj);
}
}
return ret;
};
return exports;
})({});
/* ------------------------------ defaults ------------------------------ */
var defaults = _.defaults = (function (exports) {
/* Fill in undefined properties in object with the first value present in the following list of defaults objects.
*
* |Name |Desc |
* |------|------------------|
* |obj |Destination object|
* |...src|Sources objects |
* |return|Destination object|
*/
/* example
* defaults({ name: 'RedHood' }, { name: 'Unknown', age: 24 }); // -> {name: 'RedHood', age: 24}
*/
/* typescript
* export declare function defaults(obj: any, ...src: any[]): any;
*/
/* dependencies
* createAssigner allKeys
*/
exports = createAssigner(allKeys, true);
return exports;
})({});
/* ------------------------------ extend ------------------------------ */
var extend = _.extend = (function (exports) {
/* Copy all of the properties in the source objects over to the destination object.
*
* |Name |Desc |
* |-----------|------------------|
* |destination|Destination object|
* |...sources |Sources objects |
* |return |Destination object|
*/
/* example
* extend({ name: 'RedHood' }, { age: 24 }); // -> {name: 'RedHood', age: 24}
*/
/* typescript
* export declare function extend(destination: any, ...sources: any[]): any;
*/
/* dependencies
* createAssigner allKeys
*/
exports = createAssigner(allKeys);
return exports;
})({});
/* ------------------------------ map ------------------------------ */
var map = _.map = (function (exports) {
/* Create an array of values by running each element in collection through iteratee.
*
* |Name |Desc |
* |--------|------------------------------|
* |object |Collection to iterate over |
* |iterator|Function invoked per iteration|
* |context |Function context |
* |return |New mapped array |
*/
/* example
* map([4, 8], function(n) {
* return n * n;
* }); // -> [16, 64]
*/
/* typescript
* export declare function map(
* list: types.List,
* iterator: types.ListIterator,
* context?: any
* ): TResult[];
* export declare function map(
* object: types.Dictionary,
* iterator: types.ObjectIterator,
* context?: any
* ): TResult[];
*/
/* dependencies
* safeCb keys isArrLike types
*/
exports = function(obj, iterator, ctx) {
iterator = safeCb(iterator, ctx);
var _keys = !isArrLike(obj) && keys(obj);
var len = (_keys || obj).length;
var results = Array(len);
for (var i = 0; i < len; i++) {
var curKey = _keys ? _keys[i] : i;
results[i] = iterator(obj[curKey], curKey, obj);
}
return results;
};
return exports;
})({});
/* ------------------------------ toArr ------------------------------ */
var toArr = _.toArr = (function (exports) {
/* Convert value to an array.
*
* |Name |Desc |
* |------|----------------|
* |val |Value to convert|
* |return|Converted array |
*/
/* example
* toArr({ a: 1, b: 2 }); // -> [{a: 1, b: 2}]
* toArr('abc'); // -> ['abc']
* toArr(1); // -> [1]
* toArr(null); // -> []
*/
/* typescript
* export declare function toArr(val: any): any[];
*/
/* dependencies
* isArrLike map isArr isStr
*/
exports = function(val) {
if (!val) return [];
if (isArr(val)) return val;
if (isArrLike(val) && !isStr(val)) return map(val);
return [val];
};
return exports;
})({});
/* ------------------------------ Class ------------------------------ */
var Class = _.Class = (function (exports) {
/* Create JavaScript class.
*
* |Name |Desc |
* |-------|---------------------------------|
* |methods|Public methods |
* [statics|Static methods |
* |return |Function used to create instances|
*/
/* example
* const People = Class({
* initialize: function People(name, age) {
* this.name = name;
* this.age = age;
* },
* introduce: function() {
* return 'I am ' + this.name + ', ' + this.age + ' years old.';
* }
* });
*
* const Student = People.extend(
* {
* initialize: function Student(name, age, school) {
* this.callSuper(People, 'initialize', arguments);
*
* this.school = school;
* },
* introduce: function() {
* return (
* this.callSuper(People, 'introduce') +
* '\n I study at ' +
* this.school +
* '.'
* );
* }
* },
* {
* is: function(obj) {
* return obj instanceof Student;
* }
* }
* );
*
* const a = new Student('allen', 17, 'Hogwarts');
* a.introduce(); // -> 'I am allen, 17 years old. \n I study at Hogwarts.'
* Student.is(a); // -> true
*/
/* typescript
* export declare namespace Class {
* class Base {
* toString(): string;
* }
* class IConstructor extends Base {
* constructor(...args: any[]);
* static extend(methods: any, statics: any): IConstructor;
* static inherits(Class: types.AnyFn): void;
* static methods(methods: any): IConstructor;
* static statics(statics: any): IConstructor;
* [method: string]: any;
* }
* }
* export declare function Class(methods: any, statics?: any): Class.IConstructor;
*/
/* dependencies
* extend toArr inherits safeGet isMiniProgram types
*/
exports = function(methods, statics) {
return Base.extend(methods, statics);
};
function makeClass(parent, methods, statics) {
statics = statics || {};
var className =
methods.className || safeGet(methods, 'initialize.name') || '';
delete methods.className;
var ctor = function() {
var args = toArr(arguments);
return this.initialize
? this.initialize.apply(this, args) || this
: this;
};
if (!isMiniProgram) {
// unsafe-eval CSP violation
try {
ctor = new Function(
'toArr',
'return function ' +
className +
'()' +
'{' +
'var args = toArr(arguments);' +
'return this.initialize ? this.initialize.apply(this, args) || this : this;' +
'};'
)(toArr);
} catch (e) {
/* eslint-disable no-empty */
}
}
inherits(ctor, parent);
ctor.prototype.constructor = ctor;
ctor.extend = function(methods, statics) {
return makeClass(ctor, methods, statics);
};
ctor.inherits = function(Class) {
inherits(ctor, Class);
};
ctor.methods = function(methods) {
extend(ctor.prototype, methods);
return ctor;
};
ctor.statics = function(statics) {
extend(ctor, statics);
return ctor;
};
ctor.methods(methods).statics(statics);
return ctor;
}
var Base = (exports.Base = makeClass(Object, {
className: 'Base',
callSuper: function(parent, name, args) {
var superMethod = parent.prototype[name];
return superMethod.apply(this, args);
},
toString: function() {
return this.constructor.name;
}
}));
return exports;
})({});
/* ------------------------------ ucs2 ------------------------------ */
var ucs2 = _.ucs2 = (function (exports) {
/* UCS-2 encoding and decoding.
*
* ### encode
*
* Create a string using an array of code point values.
*
* |Name |Desc |
* |------|--------------------|
* |arr |Array of code points|
* |return|Encoded string |
*
* ### decode
*
* Create an array of code point values using a string.
*
* |Name |Desc |
* |------|--------------------|
* |str |Input string |
* |return|Array of code points|
*/
/* example
* ucs2.encode([0x61, 0x62, 0x63]); // -> 'abc'
* ucs2.decode('abc'); // -> [0x61, 0x62, 0x63]
* '𝌆'.length; // -> 2
* ucs2.decode('𝌆').length; // -> 1
*/
/* typescript
* export declare const ucs2: {
* encode(arr: number[]): string;
* decode(str: string): number[];
* };
*/
/* dependencies
* chunk map
*/ // https://mathiasbynens.be/notes/javascript-encoding
exports = {
encode: function(arr) {
// https://stackoverflow.com/questions/22747068/is-there-a-max-number-of-arguments-javascript-functions-can-accept
if (arr.length < 32768) {
return String.fromCodePoint.apply(String, arr);
}
return map(chunk(arr, 32767), function(nums) {
return String.fromCodePoint.apply(String, nums);
}).join('');
},
decode: function(str) {
var ret = [];
var i = 0;
var len = str.length;
while (i < len) {
var c = str.charCodeAt(i++); // A high surrogate
if (c >= 0xd800 && c <= 0xdbff && i < len) {
var tail = str.charCodeAt(i++); // nextC >= 0xDC00 && nextC <= 0xDFFF
if ((tail & 0xfc00) === 0xdc00) {
// C = (H - 0xD800) * 0x400 + L - 0xDC00 + 0x10000
ret.push(((c & 0x3ff) << 10) + (tail & 0x3ff) + 0x10000);
} else {
ret.push(c);
i--;
}
} else {
ret.push(c);
}
}
return ret;
}
};
return exports;
})({});
/* ------------------------------ utf8 ------------------------------ */
var utf8 = _.utf8 = (function (exports) {
/* UTF-8 encoding and decoding.
*
* ### encode
*
* Turn any UTF-8 decoded string into UTF-8 encoded string.
*
* |Name |Desc |
* |------|----------------|
* |str |String to encode|
* |return|Encoded string |
*
* ### decode
*
* Turn any UTF-8 encoded string into UTF-8 decoded string.
*
* |Name |Desc |
* |----------|----------------------|
* |str |String to decode |
* |safe=false|Suppress error if true|
* |return |Decoded string |
*/
/* example
* utf8.encode('\uD800\uDC00'); // -> '\xF0\x90\x80\x80'
* utf8.decode('\xF0\x90\x80\x80'); // -> '\uD800\uDC00'
*/
/* typescript
* export declare const utf8: {
* encode(str: string): string;
* decode(str: string, safe?: boolean): string;
* };
*/
/* dependencies
* ucs2
*/ // https://encoding.spec.whatwg.org/#utf-8
exports = {
encode: function(str) {
var codePoints = ucs2.decode(str);
var byteArr = '';
for (var i = 0, len = codePoints.length; i < len; i++) {
byteArr += encodeCodePoint(codePoints[i]);
}
return byteArr;
},
decode: function(str, safe) {
byteArr = ucs2.decode(str);
byteIdx = 0;
byteCount = byteArr.length;
codePoint = 0;
bytesSeen = 0;
bytesNeeded = 0;
lowerBoundary = 0x80;
upperBoundary = 0xbf;
var codePoints = [];
var tmp;
while ((tmp = decodeCodePoint(safe)) !== false) {
codePoints.push(tmp);
}
return ucs2.encode(codePoints);
}
};
var fromCharCode = String.fromCharCode;
function encodeCodePoint(codePoint) {
// U+0000 to U+0080, ASCII code point
if ((codePoint & 0xffffff80) === 0) {
return fromCharCode(codePoint);
}
var ret = '',
count,
offset; // U+0080 to U+07FF, inclusive
if ((codePoint & 0xfffff800) === 0) {
count = 1;
offset = 0xc0;
} else if ((codePoint & 0xffff0000) === 0) {
// U+0800 to U+FFFF, inclusive
count = 2;
offset = 0xe0;
} else if ((codePoint & 0xffe00000) == 0) {
// U+10000 to U+10FFFF, inclusive
count = 3;
offset = 0xf0;
}
ret += fromCharCode((codePoint >> (6 * count)) + offset);
while (count > 0) {
var tmp = codePoint >> (6 * (count - 1));
ret += fromCharCode(0x80 | (tmp & 0x3f));
count--;
}
return ret;
}
var byteArr,
byteIdx,
byteCount,
codePoint,
bytesSeen,
bytesNeeded,
lowerBoundary,
upperBoundary;
function decodeCodePoint(safe) {
/* eslint-disable no-constant-condition */
while (true) {
if (byteIdx >= byteCount && bytesNeeded) {
if (safe) return goBack();
throw new Error('Invalid byte index');
}
if (byteIdx === byteCount) return false;
var byte = byteArr[byteIdx];
byteIdx++;
if (!bytesNeeded) {
// 0x00 to 0x7F
if ((byte & 0x80) === 0) {
return byte;
} // 0xC2 to 0xDF
if ((byte & 0xe0) === 0xc0) {
bytesNeeded = 1;
codePoint = byte & 0x1f;
} else if ((byte & 0xf0) === 0xe0) {
// 0xE0 to 0xEF
if (byte === 0xe0) lowerBoundary = 0xa0;
if (byte === 0xed) upperBoundary = 0x9f;
bytesNeeded = 2;
codePoint = byte & 0xf;
} else if ((byte & 0xf8) === 0xf0) {
// 0xF0 to 0xF4
if (byte === 0xf0) lowerBoundary = 0x90;
if (byte === 0xf4) upperBoundary = 0x8f;
bytesNeeded = 3;
codePoint = byte & 0x7;
} else {
if (safe) return goBack();
throw new Error('Invalid UTF-8 detected');
}
continue;
}
if (byte < lowerBoundary || byte > upperBoundary) {
if (safe) {
byteIdx--;
return goBack();
}
throw new Error('Invalid continuation byte');
}
lowerBoundary = 0x80;
upperBoundary = 0xbf;
codePoint = (codePoint << 6) | (byte & 0x3f);
bytesSeen++;
if (bytesSeen !== bytesNeeded) continue;
var tmp = codePoint;
codePoint = 0;
bytesNeeded = 0;
bytesSeen = 0;
return tmp;
}
}
function goBack() {
var start = byteIdx - bytesSeen - 1;
byteIdx = start + 1;
codePoint = 0;
bytesNeeded = 0;
bytesSeen = 0;
lowerBoundary = 0x80;
upperBoundary = 0xbf;
return byteArr[start];
}
return exports;
})({});
/* ------------------------------ decodeUriComponent ------------------------------ */
var decodeUriComponent = _.decodeUriComponent = (function (exports) {
/* Better decodeURIComponent that does not throw if input is invalid.
*
* |Name |Desc |
* |------|----------------|
* |str |String to decode|
* |return|Decoded string |
*/
/* example
* decodeUriComponent('%%25%'); // -> '%%%'
* decodeUriComponent('%E0%A4%A'); // -> '\xE0\xA4%A'
*/
/* typescript
* export declare function decodeUriComponent(str: string): string;
*/
/* dependencies
* each ucs2 map utf8
*/
exports = function(str) {
try {
return decodeURIComponent(str);
} catch (e) {
var matches = str.match(regMatcher);
if (!matches) {
return str;
}
each(matches, function(match) {
str = str.replace(match, decode(match));
});
return str;
}
};
function decode(str) {
str = str.split('%').slice(1);
var bytes = map(str, hexToInt);
str = ucs2.encode(bytes);
str = utf8.decode(str, true);
return str;
}
function hexToInt(numStr) {
return +('0x' + numStr);
}
var regMatcher = /(%[a-f0-9]{2})+/gi;
return exports;
})({});
/* ------------------------------ cookie ------------------------------ */
_.cookie = (function (exports) {
/* Simple api for handling browser cookies.
*
* ### get
*
* Get cookie value.
*
* |Name |Desc |
* |------|--------------------------|
* |key |Cookie key |
* |return|Corresponding cookie value|
*
* ### set
*
* Set cookie value.
*
* |Name |Desc |
* |-------|--------------|
* |key |Cookie key |
* |val |Cookie value |
* |options|Cookie options|
* |return |Module cookie |
*
* ### remove
*
* Remove cookie value.
*
* |Name |Desc |
* |-------|--------------|
* |key |Cookie key |
* |options|Cookie options|
* |return |Module cookie |
*/
/* example
* cookie.set('a', '1', { path: '/' });
* cookie.get('a'); // -> '1'
* cookie.remove('a');
*/
/* typescript
* export declare namespace cookie {
* interface IOptions {
* path?: string;
* expires?: number;
* domain?: string;
* secure?: boolean;
* }
* interface ICookie {
* get(key: string, options?: cookie.IOptions): string;
* set(key: string, val: string, options?: cookie.IOptions): ICookie;
* remove(key: string, options?: cookie.IOptions): ICookie;
* }
* }
* export declare const cookie: cookie.ICookie;
*/
/* dependencies
* defaults isNum isUndef decodeUriComponent
*/
var defOpts = {
path: '/'
};
function setCookie(key, val, options) {
if (!isUndef(val)) {
options = options || {};
options = defaults(options, defOpts);
if (isNum(options.expires)) {
var expires = new Date();
expires.setMilliseconds(
expires.getMilliseconds() + options.expires * 864e5
);
options.expires = expires;
}
val = encodeURIComponent(val);
key = encodeURIComponent(key);
document.cookie = [
key,
'=',
val,
options.expires && '; expires=' + options.expires.toUTCString(),
options.path && '; path=' + options.path,
options.domain && '; domain=' + options.domain,
options.secure ? '; secure' : ''
].join('');
return exports;
}
var cookies = document.cookie ? document.cookie.split('; ') : [];
var result = key ? undefined : {};
for (var i = 0, len = cookies.length; i < len; i++) {
var c = cookies[i];
var parts = c.split('=');
var name = decodeUriComponent(parts.shift());
c = parts.join('=');
c = decodeUriComponent(c);
if (key === name) {
result = c;
break;
}
if (!key) result[name] = c;
}
return result;
}
exports = {
get: setCookie,
set: setCookie,
remove: function(key, options) {
options = options || {};
options.expires = -1;
return setCookie(key, '', options);
}
};
return exports;
})({});
/* ------------------------------ rtrim ------------------------------ */
var rtrim = _.rtrim = (function (exports) {
/* Remove chars or white-spaces from end of string.
*
* |Name |Desc |
* |------|------------------|
* |str |String to trim |
* |chars |Characters to trim|
* |return|Trimmed string |
*/
/* example
* rtrim(' abc '); // -> ' abc'
* rtrim('_abc_', '_'); // -> '_abc'
* rtrim('_abc_', ['c', '_']); // -> '_ab'
*/
/* typescript
* export declare function rtrim(str: string, chars?: string | string[]): string;
*/
exports = function(str, chars) {
if (chars == null) {
if (str.trimRight) {
return str.trimRight();
}
chars = ' \r\n\t\f\v';
}
var end = str.length - 1;
var charLen = chars.length;
var found = true;
var i;
var c;
while (found && end >= 0) {
found = false;
i = -1;
c = str.charAt(end);
while (++i < charLen) {
if (c === chars[i]) {
found = true;
end--;
break;
}
}
}
return end >= 0 ? str.substring(0, end + 1) : '';
};
return exports;
})({});
/* ------------------------------ trim ------------------------------ */
var trim = _.trim = (function (exports) {
/* Remove chars or white-spaces from beginning end of string.
*
* |Name |Desc |
* |------|------------------|
* |str |String to trim |
* |chars |Characters to trim|
* |return|Trimmed string |
*/
/* example
* trim(' abc '); // -> 'abc'
* trim('_abc_', '_'); // -> 'abc'
* trim('_abc_', ['a', 'c', '_']); // -> 'b'
*/
/* typescript
* export declare function trim(str: string, chars?: string | string[]): string;
*/
/* dependencies
* ltrim rtrim
*/
exports = function(str, chars) {
if (chars == null && str.trim) {
return str.trim();
}
return ltrim(rtrim(str, chars), chars);
};
return exports;
})({});
/* ------------------------------ query ------------------------------ */
var query = _.query = (function (exports) {
/* Parse and stringify url query strings.
*
* ### parse
*
* Parse a query string into an object.
*
* |Name |Desc |
* |------|------------|
* |str |Query string|
* |return|Query object|
*
* ### stringify
*
* Stringify an object into a query string.
*
* |Name |Desc |
* |------|------------|
* |obj |Query object|
* |return|Query string|
*/
/* example
* query.parse('foo=bar&eruda=true'); // -> {foo: 'bar', eruda: 'true'}
* query.stringify({ foo: 'bar', eruda: 'true' }); // -> 'foo=bar&eruda=true'
* query.parse('name=eruda&name=eustia'); // -> {name: ['eruda', 'eustia']}
*/
/* typescript
* export declare const query: {
* parse(str: string): any;
* stringify(object: any): string;
* };
*/
/* dependencies
* trim each isUndef isArr map isEmpty filter isObj
*/
exports = {
parse: function(str) {
var ret = {};
str = trim(str).replace(regIllegalChars, '');
each(str.split('&'), function(param) {
var parts = param.split('=');
var key = parts.shift(),
val = parts.length > 0 ? parts.join('=') : null;
key = decodeURIComponent(key);
val = decodeURIComponent(val);
if (isUndef(ret[key])) {
ret[key] = val;
} else if (isArr(ret[key])) {
ret[key].push(val);
} else {
ret[key] = [ret[key], val];
}
});
return ret;
},
stringify: function(obj, arrKey) {
return filter(
map(obj, function(val, key) {
if (isObj(val) && isEmpty(val)) return '';
if (isArr(val)) return exports.stringify(val, key);
return (
(arrKey
? encodeURIComponent(arrKey)
: encodeURIComponent(key)) +
'=' +
encodeURIComponent(val)
);
}),
function(str) {
return str.length > 0;
}
).join('&');
}
};
var regIllegalChars = /^(\?|#|&)/g;
return exports;
})({});
/* ------------------------------ ajax ------------------------------ */
_.ajax = (function (exports) {
/* Perform an asynchronous HTTP request.
*
* |Name |Desc |
* |-------|------------|
* |options|Ajax options|
*
* Available options:
*
* |Name |Desc |
* |---------------------------------------------|---------------------------|
* |type=get |Request type |
* |url |Request url |
* |data |Request data |
* |dataType=json |Response type(json, xml) |
* |contentType=application/x-www-form-urlencoded|Request header Content-Type|
* |success |Success callback |
* |error |Error callback |
* |complete |Callback after request |
* |timeout |Request timeout |
*
* ### get
*
* Shortcut for type = GET;
*
* ### post
*
* Shortcut for type = POST;
*
* |Name |Desc |
* |--------|----------------|
* |url |Request url |
* |data |Request data |
* |success |Success callback|
* |dataType|Response type |
*/
/* example
* ajax({
* url: 'http://example.com',
* data: { test: 'true' },
* error() {},
* success(data) {
* // ...
* },
* dataType: 'json'
* });
*
* ajax.get('http://example.com', {}, function(data) {
* // ...
* });
*/
/* typescript
* export declare namespace ajax {
* function get(
* url: string,
* data: string | {},
* success: types.AnyFn,
* dataType?: string
* ): XMLHttpRequest;
* function get(
* url: string,
* success: types.AnyFn,
* dataType?: string
* ): XMLHttpRequest;
* function post(
* url: string,
* data: string | {},
* success: types.AnyFn,
* dataType?: string
* ): XMLHttpRequest;
* function post(
* url: string,
* success: types.AnyFn,
* dataType?: string
* ): XMLHttpRequest;
* }
* export declare function ajax(options: {
* type?: string;
* url: string;
* data?: string | {};
* dataType?: string;
* contentType?: string;
* success?: types.AnyFn;
* error?: types.AnyFn;
* complete?: types.AnyFn;
* timeout?: number;
* }): XMLHttpRequest;
*/
/* dependencies
* isFn noop defaults isObj query types
*/
exports = function(options) {
defaults(options, exports.setting);
var type = options.type;
var url = options.url;
var data = options.data;
var dataType = options.dataType;
var success = options.success;
var error = options.error;
var timeout = options.timeout;
var complete = options.complete;
var xhr = options.xhr();
var abortTimeout;
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
clearTimeout(abortTimeout);
var result;
var status = xhr.status;
if ((status >= 200 && status < 300) || status === 304) {
result = xhr.responseText;
if (dataType === 'xml') result = xhr.responseXML;
try {
if (dataType === 'json') result = JSON.parse(result);
/* eslint-disable no-empty */
} catch (e) {}
success(result, xhr);
} else {
error(xhr);
}
complete(xhr);
};
if (type === 'GET') {
data = query.stringify(data);
if (data) url += url.indexOf('?') > -1 ? '&' + data : '?' + data;
} else if (options.contentType === 'application/x-www-form-urlencoded') {
if (isObj(data)) data = query.stringify(data);
} else if (options.contentType === 'application/json') {
if (isObj(data)) data = JSON.stringify(data);
}
xhr.open(type, url, true);
xhr.setRequestHeader('Content-Type', options.contentType);
if (timeout > 0) {
abortTimeout = setTimeout(function() {
xhr.onreadystatechange = noop;
xhr.abort();
error(xhr, 'timeout');
complete(xhr);
}, timeout);
}
xhr.send(type === 'GET' ? null : data);
return xhr;
};
exports.setting = {
type: 'GET',
success: noop,
error: noop,
complete: noop,
dataType: 'json',
contentType: 'application/x-www-form-urlencoded',
data: {},
xhr: function() {
return new XMLHttpRequest();
},
timeout: 0
};
exports.get = function() {
return exports(parseArgs.apply(null, arguments));
};
exports.post = function() {
var options = parseArgs.apply(null, arguments);
options.type = 'POST';
return exports(options);
};
function parseArgs(url, data, success, dataType) {
if (isFn(data)) {
dataType = success;
success = data;
data = {};
}
return {
url: url,
data: data,
success: success,
dataType: dataType
};
}
return exports;
})({});
/* ------------------------------ safeSet ------------------------------ */
var safeSet = _.safeSet = (function (exports) {
/* Set value at path of object.
*
* If a portion of path doesn't exist, it's created.
*
* |Name|Desc |
* |----|-----------------------|
* |obj |Object to modify |
* |path|Path of property to set|
* |val |Value to set |
*/
/* example
* const obj = {};
* safeSet(obj, 'a.aa.aaa', 1); // obj = {a: {aa: {aaa: 1}}}
* safeSet(obj, ['a', 'aa'], 2); // obj = {a: {aa: 2}}
* safeSet(obj, 'a.b', 3); // obj = {a: {aa: 2, b: 3}}
*/
/* typescript
* export declare function safeSet(
* obj: any,
* path: string | string[],
* val: any
* ): void;
*/
/* dependencies
* castPath isUndef toStr isSymbol isStr
*/
exports = function(obj, path, val) {
path = castPath(path, obj);
var lastProp = path.pop();
var prop;
prop = path.shift();
while (!isUndef(prop)) {
// #25
if (!isStr(prop) && !isSymbol(prop)) {
prop = toStr(prop);
}
if (
prop === '__proto__' ||
prop === 'constructor' ||
prop === 'prototype'
) {
return;
}
if (!obj[prop]) obj[prop] = {};
obj = obj[prop];
prop = path.shift();
}
obj[lastProp] = val;
};
return exports;
})({});
/* ------------------------------ startWith ------------------------------ */
var startWith = _.startWith = (function (exports) {
/* Check if string starts with the given target string.
*
* |Name |Desc |
* |------|---------------------------------|
* |str |String to search |
* |prefix|String prefix |
* |return|True if string starts with prefix|
*/
/* example
* startWith('ab', 'a'); // -> true
*/
/* typescript
* export declare function startWith(str: string, prefix: string): boolean;
*/
exports = function(str, prefix) {
return str.indexOf(prefix) === 0;
};
return exports;
})({});
/* ------------------------------ type ------------------------------ */
var type = _.type = (function (exports) {
/* Determine the internal JavaScript [[Class]] of an object.
*
* |Name |Desc |
* |--------------|-----------------|
* |val |Value to get type|
* |lowerCase=true|LowerCase result |
* |return |Type of object |
*/
/* example
* type(5); // -> 'number'
* type({}); // -> 'object'
* type(function() {}); // -> 'function'
* type([]); // -> 'array'
* type([], false); // -> 'Array'
* type(async function() {}, false); // -> 'AsyncFunction'
*/
/* typescript
* export declare function type(val: any, lowerCase?: boolean): string;
*/
/* dependencies
* objToStr isNaN lowerCase isBuffer
*/
exports = function(val) {
var lower =
arguments.length > 1 && arguments[1] !== undefined
? arguments[1]
: true;
var ret;
if (val === null) ret = 'Null';
if (val === undefined) ret = 'Undefined';
if (isNaN(val)) ret = 'NaN';
if (isBuffer(val)) ret = 'Buffer';
if (!ret) {
ret = objToStr(val).match(regObj);
if (ret) ret = ret[1];
}
if (!ret) return '';
return lower ? lowerCase(ret) : ret;
};
var regObj = /^\[object\s+(.*?)]$/;
return exports;
})({});
/* ------------------------------ toSrc ------------------------------ */
var toSrc = _.toSrc = (function (exports) {
/* Convert function to its source code.
*
* |Name |Desc |
* |------|-------------------|
* |fn |Function to convert|
* |return|Source code |
*/
/* example
* toSrc(Math.min); // -> 'function min() { [native code] }'
* toSrc(function() {}); // -> 'function () { }'
*/
/* typescript
* export declare function toSrc(fn: types.AnyFn): string;
*/
/* dependencies
* isNil types
*/
exports = function(fn) {
if (isNil(fn)) return '';
try {
return fnToStr.call(fn);
/* eslint-disable no-empty */
} catch (e) {}
try {
return fn + '';
/* eslint-disable no-empty */
} catch (e) {}
return '';
};
var fnToStr = Function.prototype.toString;
return exports;
})({});
/* ------------------------------ stringifyAll ------------------------------ */
_.stringifyAll = (function (exports) {
/* Stringify object into json with types.
*
* |Name |Desc |
* |-------|-------------------|
* |obj |Object to stringify|
* |options|Stringify options |
* |return |Stringified object |
*
* Available options:
*
* |Name |Desc |
* |------------------|-------------------------|
* |unenumerable=false|Include unenumerable keys|
* |symbol=false |Include symbol keys |
* |accessGetter=false|Access getter value |
* |timeout=0 |Timeout of stringify |
* |depth=0 |Max depth of recursion |
* |ignore |Values to ignore |
*
* When time is out, all remaining values will all be "Timeout".
*
* ### parse
*
* Parse result string back to object.
*
* |Name |Type |
* |------|---------------|
* |obj |String to parse|
* |return|Result object |
*/
/* example
* stringifyAll(function test() {}); // -> '{"value":"function test() {}","type":"Function",...}'
*/
/* typescript
* export declare namespace stringifyAll {
* function parse(str: string): any;
* }
* export declare function stringifyAll(
* obj: any,
* options?: {
* unenumerable?: boolean;
* symbol?: boolean;
* accessGetter?: boolean;
* timeout?: number;
* depth?: number;
* ignore?: any[];
* }
* ): string;
*/
/* dependencies
* escapeJsStr type toStr endWith toSrc keys each Class getProto difference extend isPromise filter now allKeys contain isObj isMiniProgram create startWith safeSet defineProp pick isArrLike
*/
exports = function(obj) {
var _ref =
arguments.length > 1 && arguments[1] !== undefined
? arguments[1]
: {},
self = _ref.self,
_ref$startTime = _ref.startTime,
startTime = _ref$startTime === void 0 ? now() : _ref$startTime,
_ref$timeout = _ref.timeout,
timeout = _ref$timeout === void 0 ? 0 : _ref$timeout,
_ref$depth = _ref.depth,
depth = _ref$depth === void 0 ? 0 : _ref$depth,
_ref$curDepth = _ref.curDepth,
curDepth = _ref$curDepth === void 0 ? 1 : _ref$curDepth,
_ref$visitor = _ref.visitor,
visitor = _ref$visitor === void 0 ? new Visitor() : _ref$visitor,
_ref$unenumerable = _ref.unenumerable,
unenumerable = _ref$unenumerable === void 0 ? false : _ref$unenumerable,
_ref$symbol = _ref.symbol,
symbol = _ref$symbol === void 0 ? false : _ref$symbol,
_ref$accessGetter = _ref.accessGetter,
accessGetter = _ref$accessGetter === void 0 ? false : _ref$accessGetter,
_ref$ignore = _ref.ignore,
ignore = _ref$ignore === void 0 ? [] : _ref$ignore;
var json = '';
var options = {
visitor: visitor,
unenumerable: unenumerable,
symbol: symbol,
accessGetter: accessGetter,
depth: depth,
curDepth: curDepth + 1,
timeout: timeout,
startTime: startTime,
ignore: ignore
};
var t = type(obj, false);
if (t === 'String') {
json = wrapStr(obj);
} else if (t === 'Number') {
json = toStr(obj);
if (endWith(json, 'Infinity')) {
json = '{"value":"'.concat(json, '","type":"Number"}');
}
} else if (t === 'NaN') {
json = '{"value":"NaN","type":"Number"}';
} else if (t === 'Boolean') {
json = obj ? 'true' : 'false';
} else if (t === 'Null') {
json = 'null';
} else if (t === 'Undefined') {
json = '{"type":"Undefined"}';
} else if (t === 'Symbol') {
var val = 'Symbol';
try {
val = toStr(obj);
/* eslint-disable no-empty */
} catch (e) {}
json = '{"value":'.concat(wrapStr(val), ',"type":"Symbol"}');
} else {
if (timeout && now() - startTime > timeout) {
return wrapStr('Timeout');
}
if (depth && curDepth > depth) {
return wrapStr('{...}');
}
json = '{';
var parts = [];
var visitedObj = visitor.get(obj);
var id;
if (visitedObj) {
id = visitedObj.id;
parts.push('"reference":'.concat(id));
} else {
id = visitor.set(obj);
parts.push('"id":'.concat(id));
}
parts.push('"type":"'.concat(t, '"'));
if (endWith(t, 'Function')) {
parts.push('"value":'.concat(wrapStr(toSrc(obj))));
} else if (t === 'RegExp') {
parts.push('"value":'.concat(wrapStr(obj)));
}
if (!visitedObj) {
var enumerableKeys = keys(obj);
if (enumerableKeys.length) {
parts.push(
iterateObj(
'enumerable',
enumerableKeys,
self || obj,
options
)
);
}
if (unenumerable) {
var unenumerableKeys = difference(
allKeys(obj, {
prototype: false,
unenumerable: true
}),
enumerableKeys
);
if (unenumerableKeys.length) {
parts.push(
iterateObj(
'unenumerable',
unenumerableKeys,
self || obj,
options
)
);
}
}
if (symbol) {
var symbolKeys = filter(
allKeys(obj, {
prototype: false,
symbol: true
}),
function(key) {
return typeof key === 'symbol';
}
);
if (symbolKeys.length) {
parts.push(
iterateObj('symbol', symbolKeys, self || obj, options)
);
}
}
var prototype = getProto(obj);
if (prototype && !contain(ignore, prototype)) {
var proto = '"proto":'.concat(
exports(
prototype,
extend(options, {
self: self || obj
})
)
);
parts.push(proto);
}
}
json += parts.join(',') + '}';
}
return json;
};
function iterateObj(name, keys, obj, options) {
var parts = [];
each(keys, function(key) {
var val;
var descriptor = Object.getOwnPropertyDescriptor(obj, key);
var hasGetter = descriptor && descriptor.get;
var hasSetter = descriptor && descriptor.set;
if (!options.accessGetter && hasGetter) {
val = '(...)';
} else {
try {
val = obj[key];
if (contain(options.ignore, val)) {
return;
}
if (isPromise(val)) {
val.catch(function() {});
}
} catch (e) {
val = e.message;
}
}
parts.push(''.concat(wrapKey(key), ':').concat(exports(val, options)));
if (hasGetter) {
parts.push(
''
.concat(wrapKey('get ' + toStr(key)), ':')
.concat(exports(descriptor.get, options))
);
}
if (hasSetter) {
parts.push(
''
.concat(wrapKey('set ' + toStr(key)), ':')
.concat(exports(descriptor.set, options))
);
}
});
return '"'.concat(name, '":{') + parts.join(',') + '}';
}
function wrapKey(key) {
return '"'.concat(escapeJsonStr(key), '"');
}
function wrapStr(str) {
return '"'.concat(escapeJsonStr(toStr(str)), '"');
}
function escapeJsonStr(str) {
return escapeJsStr(str)
.replace(/\\'/g, "'")
.replace(/\t/g, '\\t');
}
var Visitor = Class({
initialize: function() {
this.id = 1;
this.visited = [];
},
set: function(val) {
var visited = this.visited,
id = this.id;
var obj = {
id: id,
val: val
};
visited.push(obj);
this.id++;
return id;
},
get: function(val) {
var visited = this.visited;
for (var i = 0, len = visited.length; i < len; i++) {
var obj = visited[i];
if (val === obj.val) return obj;
}
return false;
}
});
exports.parse = function(str) {
var map = {};
var obj = parse(JSON.parse(str), {
map: map
});
correctReference(map);
return obj;
};
function correctReference(map) {
each(map, function(obj) {
var enumerableKeys = keys(obj);
for (var i = 0, len = enumerableKeys.length; i < len; i++) {
var key = enumerableKeys[i];
if (isObj(obj[key])) {
var reference = obj[key].reference;
if (reference && map[reference]) {
obj[key] = map[reference];
}
}
}
var proto = getProto(obj);
if (proto && proto.reference) {
if (map[proto.reference]) {
Object.setPrototypeOf(obj, map[proto.reference]);
}
}
});
}
function parse(obj, options) {
var map = options.map;
if (!isObj(obj)) {
return obj;
}
var id = obj.id,
type = obj.type,
value = obj.value,
proto = obj.proto,
reference = obj.reference;
var enumerable = obj.enumerable,
unenumerable = obj.unenumerable;
if (reference) {
return obj;
}
if (type === 'Number') {
if (value === 'Infinity') {
return Number.POSITIVE_INFINITY;
} else if (value === '-Infinity') {
return Number.NEGATIVE_INFINITY;
}
return NaN;
} else if (type === 'Undefined') {
return undefined;
}
var newObj;
if (type === 'Function') {
newObj = function() {};
newObj.toString = function() {
return value;
};
if (proto) {
Object.setPrototypeOf(newObj, parse(proto, options));
}
} else if (type === 'RegExp') {
newObj = strToRegExp(value);
} else {
if (type !== 'Object') {
var Fn;
if (!isMiniProgram) {
Fn = new Function(type, '');
} else {
Fn = function() {};
}
if (proto) {
Fn.prototype = parse(proto, options);
}
newObj = new Fn();
} else {
if (proto) {
newObj = create(parse(proto, options));
} else {
newObj = create(null);
}
}
}
var defineProps = {};
if (enumerable) {
var len;
if (isArrLike(enumerable)) {
len = enumerable.length;
delete enumerable.length;
}
enumerable = pick(enumerable, function(value, key) {
return !handleGetterSetter(enumerable, value, key);
});
each(enumerable, function(value, key) {
var defineProp = defineProps[key] || {};
if (!defineProp.get) {
newObj[key] = parse(value, options);
}
});
if (len) {
newObj.length = len;
}
}
if (unenumerable) {
unenumerable = pick(unenumerable, function(value, key) {
return !handleGetterSetter(unenumerable, value, key);
});
each(unenumerable, function(value, key) {
var defineProp = defineProps[key] || {};
if (!defineProp.get) {
value = parse(value, options);
if (isObj(value) && value.reference) {
var _reference = value.reference;
value = function() {
return map[_reference];
};
defineProp.get = value;
} else {
defineProp.value = value;
}
}
defineProp.enumerable = false;
defineProps[key] = defineProp;
});
}
defineProp(newObj, defineProps);
function handleGetterSetter(obj, val, key) {
key = toStr(key);
var isGetterAndSetter = false;
each(['get', 'set'], function(type) {
if (startWith(key, type + ' ')) {
var realKey = key.replace(type + ' ', '');
if (obj[realKey]) {
val = parse(val, options);
if (val === 'Timeout') {
val = retTimeout;
}
safeSet(defineProps, [realKey, type], val);
isGetterAndSetter = true;
}
}
});
return isGetterAndSetter;
}
map[id] = newObj;
return newObj;
}
function retTimeout() {
return 'Timeout';
}
function strToRegExp(str) {
var lastSlash = str.lastIndexOf('/');
return new RegExp(str.slice(1, lastSlash), str.slice(lastSlash + 1));
}
return exports;
})({});
/* ------------------------------ toEl ------------------------------ */
_.toEl = (function (exports) {
/* Convert html string to dom elements.
*
* There should be only one root element.
*
* |Name |Desc |
* |------|------------|
* |str |Html string |
* |return|Html element|
*/
/* example
* toEl('test
');
*/
/* typescript
* export declare function toEl(str: string): Element;
*/
var doc = document;
exports = function(str) {
var fragment = doc.createElement('body');
fragment.innerHTML = str;
return fragment.childNodes[0];
};
if (doc.createRange && doc.body) {
var range = doc.createRange();
range.selectNode(doc.body);
if (range.createContextualFragment) {
exports = function(str) {
return range.createContextualFragment(str).childNodes[0];
};
}
}
return exports;
})({});
return _;
}));